custom: add UserTable component

This commit is contained in:
javayhu 2025-05-10 14:14:00 +08:00
parent b1ea926aa6
commit 4dd36ed9c4
2 changed files with 627 additions and 9 deletions

View File

@ -1,10 +1,270 @@
import { ChartAreaInteractive } from '@/components/dashboard/chart-area-interactive';
import { UsersTable } from '@/components/admin/users-table';
import { DashboardHeader } from '@/components/dashboard/dashboard-header';
import { DataTable } from '@/components/dashboard/data-table';
import { SectionCards } from '@/components/dashboard/section-cards';
import { useTranslations } from 'next-intl';
import data from './data.json';
// Mock data for demonstration
const mockUsers = [
{
id: '1',
name: 'John Doe',
email: 'john@example.com',
emailVerified: true,
image: null,
role: 'admin',
createdAt: new Date('2024-01-01'),
customerId: 'cus_123',
banned: false,
banReason: null,
banExpires: null,
},
{
id: '2',
name: 'Jane Smith',
email: 'jane@example.com',
emailVerified: true,
image: null,
role: 'user',
createdAt: new Date('2024-01-02'),
customerId: 'cus_456',
banned: true,
banReason: 'Violation of terms',
banExpires: new Date('2024-12-31'),
},
{
id: '3',
name: 'Alice Johnson',
email: 'alice.johnson@example.com',
emailVerified: false,
image: null,
role: 'user',
createdAt: new Date('2024-01-03'),
customerId: 'cus_789',
banned: false,
banReason: null,
banExpires: null,
},
{
id: '4',
name: 'Bob Lee',
email: 'bob.lee@example.com',
emailVerified: true,
image: null,
role: 'moderator',
createdAt: new Date('2024-01-04'),
customerId: 'cus_101',
banned: false,
banReason: null,
banExpires: null,
},
{
id: '5',
name: 'Cathy Brown',
email: 'cathy.brown@example.com',
emailVerified: false,
image: null,
role: 'user',
createdAt: new Date('2024-01-05'),
customerId: 'cus_102',
banned: true,
banReason: 'Spam',
banExpires: new Date('2024-07-01'),
},
{
id: '6',
name: 'David Kim',
email: 'david.kim@example.com',
emailVerified: true,
image: null,
role: 'user',
createdAt: new Date('2024-01-06'),
customerId: 'cus_103',
banned: false,
banReason: null,
banExpires: null,
},
{
id: '7',
name: 'Eva Green',
email: 'eva.green@example.com',
emailVerified: true,
image: null,
role: 'user',
createdAt: new Date('2024-01-07'),
customerId: 'cus_104',
banned: false,
banReason: null,
banExpires: null,
},
{
id: '8',
name: 'Frank White',
email: 'frank.white@example.com',
emailVerified: false,
image: null,
role: 'user',
createdAt: new Date('2024-01-08'),
customerId: 'cus_105',
banned: true,
banReason: 'Abuse',
banExpires: new Date('2024-08-01'),
},
{
id: '9',
name: 'Grace Black',
email: 'grace.black@example.com',
emailVerified: true,
image: null,
role: 'user',
createdAt: new Date('2024-01-09'),
customerId: 'cus_106',
banned: false,
banReason: null,
banExpires: null,
},
{
id: '10',
name: 'Henry Adams',
email: 'henry.adams@example.com',
emailVerified: false,
image: null,
role: 'user',
createdAt: new Date('2024-01-10'),
customerId: 'cus_107',
banned: false,
banReason: null,
banExpires: null,
},
{
id: '11',
name: 'Ivy Scott',
email: 'ivy.scott@example.com',
emailVerified: true,
image: null,
role: 'user',
createdAt: new Date('2024-01-11'),
customerId: 'cus_108',
banned: false,
banReason: null,
banExpires: null,
},
{
id: '12',
name: 'Jack Turner',
email: 'jack.turner@example.com',
emailVerified: true,
image: null,
role: 'user',
createdAt: new Date('2024-01-12'),
customerId: 'cus_109',
banned: false,
banReason: null,
banExpires: null,
},
{
id: '13',
name: 'Kathy Young',
email: 'kathy.young@example.com',
emailVerified: false,
image: null,
role: 'user',
createdAt: new Date('2024-01-13'),
customerId: 'cus_110',
banned: false,
banReason: null,
banExpires: null,
},
{
id: '14',
name: 'Leo King',
email: 'leo.king@example.com',
emailVerified: true,
image: null,
role: 'user',
createdAt: new Date('2024-01-14'),
customerId: 'cus_111',
banned: false,
banReason: null,
banExpires: null,
},
{
id: '15',
name: 'Mona Hall',
email: 'mona.hall@example.com',
emailVerified: false,
image: null,
role: 'user',
createdAt: new Date('2024-01-15'),
customerId: 'cus_112',
banned: true,
banReason: 'Multiple violations',
banExpires: new Date('2024-09-01'),
},
{
id: '16',
name: 'Nina Clark',
email: 'nina.clark@example.com',
emailVerified: true,
image: null,
role: 'user',
createdAt: new Date('2024-01-16'),
customerId: 'cus_113',
banned: false,
banReason: null,
banExpires: null,
},
{
id: '17',
name: 'Oscar Lewis',
email: 'oscar.lewis@example.com',
emailVerified: false,
image: null,
role: 'user',
createdAt: new Date('2024-01-17'),
customerId: 'cus_114',
banned: false,
banReason: null,
banExpires: null,
},
{
id: '18',
name: 'Paula Walker',
email: 'paula.walker@example.com',
emailVerified: true,
image: null,
role: 'user',
createdAt: new Date('2024-01-18'),
customerId: 'cus_115',
banned: false,
banReason: null,
banExpires: null,
},
{
id: '19',
name: 'Quinn Harris',
email: 'quinn.harris@example.com',
emailVerified: false,
image: null,
role: 'user',
createdAt: new Date('2024-01-19'),
customerId: 'cus_116',
banned: false,
banReason: null,
banExpires: null,
},
{
id: '20',
name: 'Rita Evans',
email: 'rita.evans@example.com',
emailVerified: true,
image: null,
role: 'user',
createdAt: new Date('2024-01-20'),
customerId: 'cus_117',
banned: false,
banReason: null,
banExpires: null,
},
];
/**
* Admin users page
@ -33,11 +293,7 @@ export default function AdminUsersPage() {
<div className="flex flex-1 flex-col">
<div className="@container/main flex flex-1 flex-col gap-2">
<div className="flex flex-col gap-4 py-4 md:gap-6 md:py-6">
<SectionCards />
<div className="px-4 lg:px-6">
<ChartAreaInteractive />
</div>
<DataTable data={data} />
<UsersTable data={mockUsers} />
</div>
</div>
</div>

View File

@ -0,0 +1,362 @@
'use client';
import { UserAvatar } from '@/components/layout/user-avatar';
import { Button } from '@/components/ui/button';
import {
DropdownMenu,
DropdownMenuCheckboxItem,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu';
import { Input } from '@/components/ui/input';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select';
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from '@/components/ui/table';
import {
IconChevronDown,
IconChevronLeft,
IconChevronRight,
IconChevronsLeft,
IconChevronsRight,
IconDotsVertical,
IconLayoutColumns,
} from '@tabler/icons-react';
import {
type ColumnDef,
type ColumnFiltersState,
type SortingState,
type VisibilityState,
flexRender,
getCoreRowModel,
getFilteredRowModel,
getPaginationRowModel,
getSortedRowModel,
useReactTable,
} from '@tanstack/react-table';
import { format } from 'date-fns';
import { useState } from 'react';
import { Label } from '../ui/label';
interface User {
id: string;
name: string;
email: string;
emailVerified: boolean;
image: string | null;
role: string | null;
createdAt: Date;
customerId: string | null;
banned: boolean | null;
banReason: string | null;
banExpires: Date | null;
}
const columns: ColumnDef<User>[] = [
{
accessorKey: 'name',
header: 'Name',
cell: ({ row }) => {
const user = row.original;
return (
<div className="flex items-center gap-2">
<UserAvatar
name={user.name}
image={user.image}
className="size-8 border"
/>
<span>{user.name}</span>
</div>
);
},
},
{
accessorKey: 'email',
header: 'Email',
},
{
accessorKey: 'emailVerified',
header: 'EmailVerified',
cell: ({ row }) => (row.original.emailVerified ? 'Yes' : 'No'),
},
{
accessorKey: 'role',
header: 'Role',
},
{
accessorKey: 'createdAt',
header: 'Created At',
cell: ({ row }) => format(new Date(row.original.createdAt), 'PP'),
},
{
accessorKey: 'customerId',
header: 'Customer ID',
},
{
accessorKey: 'banned',
header: 'Status',
cell: ({ row }) => (row.original.banned ? 'Banned' : 'Active'),
},
{
accessorKey: 'banReason',
header: 'Ban Reason',
},
{
accessorKey: 'banExpires',
header: 'Ban Expires',
cell: ({ row }) =>
row.original.banExpires
? format(new Date(row.original.banExpires), 'PP')
: '-',
},
{
id: 'actions',
header: 'Actions',
cell: ({ row }) => {
const user = row.original;
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
variant="ghost"
className="data-[state=open]:bg-muted text-muted-foreground flex size-8"
size="icon"
>
<IconDotsVertical />
<span className="sr-only">Open menu</span>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-32">
{user.banned ? (
<DropdownMenuItem>Unban User</DropdownMenuItem>
) : (
<DropdownMenuItem>Ban User</DropdownMenuItem>
)}
</DropdownMenuContent>
</DropdownMenu>
);
},
},
];
interface UsersTableProps {
data: User[];
}
/**
* https://ui.shadcn.com/docs/components/data-table
*/
export function UsersTable({ data }: UsersTableProps) {
const [sorting, setSorting] = useState<SortingState>([]);
const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>([]);
const [columnVisibility, setColumnVisibility] = useState<VisibilityState>({});
const [globalFilter, setGlobalFilter] = useState('');
const [pagination, setPagination] = useState({ pageIndex: 0, pageSize: 10 });
const table = useReactTable({
data,
columns,
state: {
sorting,
columnFilters,
columnVisibility,
globalFilter,
pagination,
},
onSortingChange: setSorting,
onColumnFiltersChange: setColumnFilters,
onColumnVisibilityChange: setColumnVisibility,
onGlobalFilterChange: setGlobalFilter,
onPaginationChange: setPagination,
getCoreRowModel: getCoreRowModel(),
getFilteredRowModel: getFilteredRowModel(),
getPaginationRowModel: getPaginationRowModel(),
getSortedRowModel: getSortedRowModel(),
});
return (
<div className="w-full flex-col justify-start gap-6 space-y-4">
<div className="flex items-center justify-between px-4 lg:px-6 gap-4">
<Input
placeholder="Search users..."
value={globalFilter ?? ''}
onChange={(event) => setGlobalFilter(event.target.value)}
className="max-w-sm"
/>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline" size="sm" className="cursor-pointer">
<IconLayoutColumns />
<span className="hidden lg:inline">Customize Columns</span>
<span className="lg:hidden">Columns</span>
<IconChevronDown />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-56">
{table
.getAllColumns()
.filter((column) => column.getCanHide())
.map((column) => {
return (
<DropdownMenuCheckboxItem
key={column.id}
className="capitalize"
checked={column.getIsVisible()}
onCheckedChange={(value) =>
column.toggleVisibility(!!value)
}
>
{column.id}
</DropdownMenuCheckboxItem>
);
})}
</DropdownMenuContent>
</DropdownMenu>
</div>
<div className="relative flex flex-col gap-4 overflow-auto px-4 lg:px-6">
<div className="overflow-hidden rounded-lg border">
<Table>
<TableHeader className="bg-muted sticky top-0 z-10">
{table.getHeaderGroups().map((headerGroup) => (
<TableRow key={headerGroup.id}>
{headerGroup.headers.map((header) => {
return (
<TableHead key={header.id}>
{header.isPlaceholder
? null
: flexRender(
header.column.columnDef.header,
header.getContext()
)}
</TableHead>
);
})}
</TableRow>
))}
</TableHeader>
<TableBody>
{table.getRowModel().rows?.length ? (
table.getRowModel().rows.map((row) => (
<TableRow
key={row.id}
data-state={row.getIsSelected() && 'selected'}
>
{row.getVisibleCells().map((cell) => (
<TableCell key={cell.id} className="py-4">
{flexRender(
cell.column.columnDef.cell,
cell.getContext()
)}
</TableCell>
))}
</TableRow>
))
) : (
<TableRow>
<TableCell
colSpan={columns.length}
className="h-24 text-center"
>
No results.
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
</div>
<div className="flex items-center justify-between px-4">
<div className="text-muted-foreground hidden flex-1 text-sm lg:flex">
{/* empty here for now */}
</div>
<div className="flex w-full items-center gap-8 lg:w-fit">
<div className="hidden items-center gap-2 lg:flex">
<Label htmlFor="rows-per-page" className="text-sm font-medium">
Rows per page
</Label>
<Select
value={`${table.getState().pagination.pageSize}`}
onValueChange={(value) => {
table.setPageSize(Number(value));
}}
>
<SelectTrigger
size="sm"
className="w-20 cursor-pointer"
id="rows-per-page"
>
<SelectValue
placeholder={table.getState().pagination.pageSize}
/>
</SelectTrigger>
<SelectContent side="top">
{[10, 20, 30, 40, 50].map((pageSize) => (
<SelectItem key={pageSize} value={`${pageSize}`}>
{pageSize}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="flex w-fit items-center justify-center text-sm font-medium">
Page {table.getState().pagination.pageIndex + 1} of{' '}
{table.getPageCount()}
</div>
<div className="ml-auto flex items-center gap-2 lg:ml-0">
<Button
variant="outline"
className="cursor-pointer hidden h-8 w-8 p-0 lg:flex"
onClick={() => table.setPageIndex(0)}
disabled={!table.getCanPreviousPage()}
>
<span className="sr-only">Go to first page</span>
<IconChevronsLeft />
</Button>
<Button
variant="outline"
className="cursor-pointer size-8"
size="icon"
onClick={() => table.previousPage()}
disabled={!table.getCanPreviousPage()}
>
<span className="sr-only">Go to previous page</span>
<IconChevronLeft />
</Button>
<Button
variant="outline"
className="cursor-pointer size-8"
size="icon"
onClick={() => table.nextPage()}
disabled={!table.getCanNextPage()}
>
<span className="sr-only">Go to next page</span>
<IconChevronRight />
</Button>
<Button
variant="outline"
className="cursor-pointer hidden size-8 lg:flex"
size="icon"
onClick={() => table.setPageIndex(table.getPageCount() - 1)}
disabled={!table.getCanNextPage()}
>
<span className="sr-only">Go to last page</span>
<IconChevronsRight />
</Button>
</div>
</div>
</div>
</div>
</div>
);
}