chore: support sort table by name/email/createdAt

This commit is contained in:
javayhu 2025-05-10 23:08:48 +08:00
parent 08a3fbf704
commit 959e49b01b
3 changed files with 112 additions and 25 deletions

View File

@ -2,7 +2,7 @@
import db from '@/db';
import { user } from '@/db/schema';
import { ilike, or, sql } from 'drizzle-orm';
import { asc, desc, ilike, or, sql } from 'drizzle-orm';
import { createSafeActionClient } from 'next-safe-action';
import { z } from 'zod';
@ -14,6 +14,15 @@ const getUsersSchema = z.object({
pageIndex: z.number().min(0).default(0),
pageSize: z.number().min(1).max(100).default(10),
search: z.string().optional().default(''),
sorting: z
.array(
z.object({
id: z.string(),
desc: z.boolean(),
})
)
.optional()
.default([]),
});
// Create a safe action for getting users
@ -21,7 +30,7 @@ export const getUsersAction = actionClient
.schema(getUsersSchema)
.action(async ({ parsedInput }) => {
try {
const { pageIndex, pageSize, search } = parsedInput;
const { pageIndex, pageSize, search, sorting } = parsedInput;
const where = search
? or(ilike(user.name, `%${search}%`), ilike(user.email, `%${search}%`))
@ -29,12 +38,29 @@ export const getUsersAction = actionClient
const offset = pageIndex * pageSize;
// Get the sort configuration
const sortConfig = sorting[0];
const [items, [{ count }]] = await Promise.all([
db
.select()
.from(user)
.where(where)
.orderBy(user.createdAt)
.orderBy(
sortConfig?.id === 'name'
? sortConfig.desc
? desc(user.name)
: asc(user.name)
: sortConfig?.id === 'email'
? sortConfig.desc
? desc(user.email)
: asc(user.email)
: sortConfig?.id === 'createdAt'
? sortConfig.desc
? desc(user.createdAt)
: asc(user.createdAt)
: user.createdAt
)
.limit(pageSize)
.offset(offset),
db.select({ count: sql`count(*)` }).from(user).where(where),

View File

@ -3,6 +3,7 @@
import { getUsersAction } from '@/actions/get-users';
import type { User } from '@/components/admin/users-table';
import { UsersTable } from '@/components/admin/users-table';
import type { SortingState } from '@tanstack/react-table';
import { useTranslations } from 'next-intl';
import { useEffect, useState } from 'react';
import { toast } from 'sonner';
@ -15,12 +16,18 @@ export function UsersPageClient() {
const [data, setData] = useState<User[]>([]);
const [total, setTotal] = useState(0);
const [loading, setLoading] = useState(false);
const [sorting, setSorting] = useState<SortingState>([]);
useEffect(() => {
const fetchUsers = async () => {
try {
setLoading(true);
const result = await getUsersAction({ pageIndex, pageSize, search });
const result = await getUsersAction({
pageIndex,
pageSize,
search,
sorting,
});
if (result?.data?.success) {
setData(result.data.data?.items || []);
@ -42,7 +49,7 @@ export function UsersPageClient() {
};
fetchUsers();
}, [pageIndex, pageSize, search]);
}, [pageIndex, pageSize, search, sorting]);
return (
<>
@ -51,11 +58,12 @@ export function UsersPageClient() {
total={total}
pageIndex={pageIndex}
pageSize={pageSize}
search={search}
loading={loading}
onPageChange={setPageIndex}
onPageSizeChange={setPageSize}
onSearch={setSearch}
search={search}
loading={loading}
onSortingChange={setSorting}
/>
</>
);

View File

@ -25,7 +25,11 @@ import {
TableHeader,
TableRow,
} from '@/components/ui/table';
import { formatDate } from '@/lib/formatter';
import {
IconArrowDown,
IconArrowUp,
IconArrowsSort,
IconChevronDown,
IconChevronLeft,
IconChevronRight,
@ -46,10 +50,38 @@ import {
getSortedRowModel,
useReactTable,
} from '@tanstack/react-table';
import { format } from 'date-fns';
import { useState } from 'react';
import { Label } from '../ui/label';
interface DataTableColumnHeaderProps<TData, TValue>
extends React.HTMLAttributes<HTMLDivElement> {
column: any;
title: string;
}
function DataTableColumnHeader<TData, TValue>({
column,
title,
className,
}: DataTableColumnHeaderProps<TData, TValue>) {
if (!column.getCanSort()) {
return <div className={className}>{title}</div>;
}
return (
<div className={className}>
<Button
variant="ghost"
className="cursor-pointer flex items-center gap-2"
onClick={() => column.toggleSorting(column.getIsSorted() === 'asc')}
>
{title}
<IconArrowsSort className="h-4 w-4" />
</Button>
</div>
);
}
export interface User {
id: string;
name: string;
@ -67,11 +99,13 @@ export interface User {
const columns: ColumnDef<User>[] = [
{
accessorKey: 'name',
header: 'Name',
header: ({ column }) => (
<DataTableColumnHeader column={column} title="Name" />
),
cell: ({ row }) => {
const user = row.original;
return (
<div className="flex items-center gap-2">
<div className="flex items-center gap-2 pl-3">
<UserAvatar
name={user.name}
image={user.image}
@ -84,7 +118,13 @@ const columns: ColumnDef<User>[] = [
},
{
accessorKey: 'email',
header: 'Email',
header: ({ column }) => (
<DataTableColumnHeader column={column} title="Email" />
),
cell: ({ row }) => {
const user = row.original;
return <div className="flex items-center gap-2 pl-3">{user.email}</div>;
},
},
{
accessorKey: 'emailVerified',
@ -97,8 +137,17 @@ const columns: ColumnDef<User>[] = [
},
{
accessorKey: 'createdAt',
header: 'Created At',
cell: ({ row }) => format(new Date(row.original.createdAt), 'PP'),
header: ({ column }) => (
<DataTableColumnHeader column={column} title="Created At" />
),
cell: ({ row }) => {
const user = row.original;
return (
<div className="flex items-center gap-2 pl-3">
{formatDate(user.createdAt)}
</div>
);
},
},
{
accessorKey: 'customerId',
@ -117,9 +166,7 @@ const columns: ColumnDef<User>[] = [
accessorKey: 'banExpires',
header: 'Ban Expires',
cell: ({ row }) =>
row.original.banExpires
? format(new Date(row.original.banExpires), 'PP')
: '-',
row.original.banExpires ? formatDate(row.original.banExpires) : '-',
},
{
id: 'actions',
@ -156,11 +203,12 @@ interface UsersTableProps {
total: number;
pageIndex: number;
pageSize: number;
search: string;
loading?: boolean;
onPageChange: (page: number) => void;
onPageSizeChange: (size: number) => void;
onSearch: (search: string) => void;
search: string;
loading?: boolean;
onSortingChange?: (sorting: SortingState) => void;
}
/**
@ -171,11 +219,12 @@ export function UsersTable({
total,
pageIndex,
pageSize,
search,
loading,
onPageChange,
onPageSizeChange,
onSearch,
search,
loading,
onSortingChange,
}: UsersTableProps) {
const [sorting, setSorting] = useState<SortingState>([]);
const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>([]);
@ -191,7 +240,11 @@ export function UsersTable({
columnVisibility,
pagination: { pageIndex, pageSize },
},
onSortingChange: setSorting,
onSortingChange: (updater) => {
const next = typeof updater === 'function' ? updater(sorting) : updater;
setSorting(next);
onSortingChange?.(next);
},
onColumnFiltersChange: setColumnFilters,
onColumnVisibilityChange: setColumnVisibility,
onPaginationChange: (updater) => {
@ -207,6 +260,7 @@ export function UsersTable({
getPaginationRowModel: getPaginationRowModel(),
getSortedRowModel: getSortedRowModel(),
manualPagination: true,
manualSorting: true,
});
return (
@ -224,9 +278,8 @@ export function UsersTable({
<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>
{/* <IconLayoutColumns /> */}
<span className="inline">Columns</span>
<IconChevronDown />
</Button>
</DropdownMenuTrigger>
@ -238,7 +291,7 @@ export function UsersTable({
return (
<DropdownMenuCheckboxItem
key={column.id}
className="capitalize"
className="capitalize cursor-pointer"
checked={column.getIsVisible()}
onCheckedChange={(value) =>
column.toggleVisibility(!!value)