feat: get users and show users in table

This commit is contained in:
javayhu 2025-05-10 20:55:22 +08:00
parent 4dd36ed9c4
commit 08a3fbf704
7 changed files with 226 additions and 937 deletions

57
src/actions/get-users.ts Normal file
View File

@ -0,0 +1,57 @@
'use server';
import db from '@/db';
import { user } from '@/db/schema';
import { ilike, or, sql } from 'drizzle-orm';
import { createSafeActionClient } from 'next-safe-action';
import { z } from 'zod';
// Create a safe action client
const actionClient = createSafeActionClient();
// Define the schema for getUsers parameters
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(''),
});
// Create a safe action for getting users
export const getUsersAction = actionClient
.schema(getUsersSchema)
.action(async ({ parsedInput }) => {
try {
const { pageIndex, pageSize, search } = parsedInput;
const where = search
? or(ilike(user.name, `%${search}%`), ilike(user.email, `%${search}%`))
: undefined;
const offset = pageIndex * pageSize;
const [items, [{ count }]] = await Promise.all([
db
.select()
.from(user)
.where(where)
.orderBy(user.createdAt)
.limit(pageSize)
.offset(offset),
db.select({ count: sql`count(*)` }).from(user).where(where),
]);
return {
success: true,
data: {
items,
total: Number(count),
},
};
} catch (error) {
console.error('get users error:', error);
return {
success: false,
error: error instanceof Error ? error.message : 'Failed to fetch users',
};
}
});

View File

@ -1,614 +0,0 @@
[
{
"id": 1,
"header": "Cover page",
"type": "Cover page",
"status": "In Process",
"target": "18",
"limit": "5",
"reviewer": "Eddie Lake"
},
{
"id": 2,
"header": "Table of contents",
"type": "Table of contents",
"status": "Done",
"target": "29",
"limit": "24",
"reviewer": "Eddie Lake"
},
{
"id": 3,
"header": "Executive summary",
"type": "Narrative",
"status": "Done",
"target": "10",
"limit": "13",
"reviewer": "Eddie Lake"
},
{
"id": 4,
"header": "Technical approach",
"type": "Narrative",
"status": "Done",
"target": "27",
"limit": "23",
"reviewer": "Jamik Tashpulatov"
},
{
"id": 5,
"header": "Design",
"type": "Narrative",
"status": "In Process",
"target": "2",
"limit": "16",
"reviewer": "Jamik Tashpulatov"
},
{
"id": 6,
"header": "Capabilities",
"type": "Narrative",
"status": "In Process",
"target": "20",
"limit": "8",
"reviewer": "Jamik Tashpulatov"
},
{
"id": 7,
"header": "Integration with existing systems",
"type": "Narrative",
"status": "In Process",
"target": "19",
"limit": "21",
"reviewer": "Jamik Tashpulatov"
},
{
"id": 8,
"header": "Innovation and Advantages",
"type": "Narrative",
"status": "Done",
"target": "25",
"limit": "26",
"reviewer": "Assign reviewer"
},
{
"id": 9,
"header": "Overview of EMR's Innovative Solutions",
"type": "Technical content",
"status": "Done",
"target": "7",
"limit": "23",
"reviewer": "Assign reviewer"
},
{
"id": 10,
"header": "Advanced Algorithms and Machine Learning",
"type": "Narrative",
"status": "Done",
"target": "30",
"limit": "28",
"reviewer": "Assign reviewer"
},
{
"id": 11,
"header": "Adaptive Communication Protocols",
"type": "Narrative",
"status": "Done",
"target": "9",
"limit": "31",
"reviewer": "Assign reviewer"
},
{
"id": 12,
"header": "Advantages Over Current Technologies",
"type": "Narrative",
"status": "Done",
"target": "12",
"limit": "0",
"reviewer": "Assign reviewer"
},
{
"id": 13,
"header": "Past Performance",
"type": "Narrative",
"status": "Done",
"target": "22",
"limit": "33",
"reviewer": "Assign reviewer"
},
{
"id": 14,
"header": "Customer Feedback and Satisfaction Levels",
"type": "Narrative",
"status": "Done",
"target": "15",
"limit": "34",
"reviewer": "Assign reviewer"
},
{
"id": 15,
"header": "Implementation Challenges and Solutions",
"type": "Narrative",
"status": "Done",
"target": "3",
"limit": "35",
"reviewer": "Assign reviewer"
},
{
"id": 16,
"header": "Security Measures and Data Protection Policies",
"type": "Narrative",
"status": "In Process",
"target": "6",
"limit": "36",
"reviewer": "Assign reviewer"
},
{
"id": 17,
"header": "Scalability and Future Proofing",
"type": "Narrative",
"status": "Done",
"target": "4",
"limit": "37",
"reviewer": "Assign reviewer"
},
{
"id": 18,
"header": "Cost-Benefit Analysis",
"type": "Plain language",
"status": "Done",
"target": "14",
"limit": "38",
"reviewer": "Assign reviewer"
},
{
"id": 19,
"header": "User Training and Onboarding Experience",
"type": "Narrative",
"status": "Done",
"target": "17",
"limit": "39",
"reviewer": "Assign reviewer"
},
{
"id": 20,
"header": "Future Development Roadmap",
"type": "Narrative",
"status": "Done",
"target": "11",
"limit": "40",
"reviewer": "Assign reviewer"
},
{
"id": 21,
"header": "System Architecture Overview",
"type": "Technical content",
"status": "In Process",
"target": "24",
"limit": "18",
"reviewer": "Maya Johnson"
},
{
"id": 22,
"header": "Risk Management Plan",
"type": "Narrative",
"status": "Done",
"target": "15",
"limit": "22",
"reviewer": "Carlos Rodriguez"
},
{
"id": 23,
"header": "Compliance Documentation",
"type": "Legal",
"status": "In Process",
"target": "31",
"limit": "27",
"reviewer": "Sarah Chen"
},
{
"id": 24,
"header": "API Documentation",
"type": "Technical content",
"status": "Done",
"target": "8",
"limit": "12",
"reviewer": "Raj Patel"
},
{
"id": 25,
"header": "User Interface Mockups",
"type": "Visual",
"status": "In Process",
"target": "19",
"limit": "25",
"reviewer": "Leila Ahmadi"
},
{
"id": 26,
"header": "Database Schema",
"type": "Technical content",
"status": "Done",
"target": "22",
"limit": "20",
"reviewer": "Thomas Wilson"
},
{
"id": 27,
"header": "Testing Methodology",
"type": "Technical content",
"status": "In Process",
"target": "17",
"limit": "14",
"reviewer": "Assign reviewer"
},
{
"id": 28,
"header": "Deployment Strategy",
"type": "Narrative",
"status": "Done",
"target": "26",
"limit": "30",
"reviewer": "Eddie Lake"
},
{
"id": 29,
"header": "Budget Breakdown",
"type": "Financial",
"status": "In Process",
"target": "13",
"limit": "16",
"reviewer": "Jamik Tashpulatov"
},
{
"id": 30,
"header": "Market Analysis",
"type": "Research",
"status": "Done",
"target": "29",
"limit": "32",
"reviewer": "Sophia Martinez"
},
{
"id": 31,
"header": "Competitor Comparison",
"type": "Research",
"status": "In Process",
"target": "21",
"limit": "19",
"reviewer": "Assign reviewer"
},
{
"id": 32,
"header": "Maintenance Plan",
"type": "Technical content",
"status": "Done",
"target": "16",
"limit": "23",
"reviewer": "Alex Thompson"
},
{
"id": 33,
"header": "User Personas",
"type": "Research",
"status": "In Process",
"target": "27",
"limit": "24",
"reviewer": "Nina Patel"
},
{
"id": 34,
"header": "Accessibility Compliance",
"type": "Legal",
"status": "Done",
"target": "18",
"limit": "21",
"reviewer": "Assign reviewer"
},
{
"id": 35,
"header": "Performance Metrics",
"type": "Technical content",
"status": "In Process",
"target": "23",
"limit": "26",
"reviewer": "David Kim"
},
{
"id": 36,
"header": "Disaster Recovery Plan",
"type": "Technical content",
"status": "Done",
"target": "14",
"limit": "17",
"reviewer": "Jamik Tashpulatov"
},
{
"id": 37,
"header": "Third-party Integrations",
"type": "Technical content",
"status": "In Process",
"target": "25",
"limit": "28",
"reviewer": "Eddie Lake"
},
{
"id": 38,
"header": "User Feedback Summary",
"type": "Research",
"status": "Done",
"target": "20",
"limit": "15",
"reviewer": "Assign reviewer"
},
{
"id": 39,
"header": "Localization Strategy",
"type": "Narrative",
"status": "In Process",
"target": "12",
"limit": "19",
"reviewer": "Maria Garcia"
},
{
"id": 40,
"header": "Mobile Compatibility",
"type": "Technical content",
"status": "Done",
"target": "28",
"limit": "31",
"reviewer": "James Wilson"
},
{
"id": 41,
"header": "Data Migration Plan",
"type": "Technical content",
"status": "In Process",
"target": "19",
"limit": "22",
"reviewer": "Assign reviewer"
},
{
"id": 42,
"header": "Quality Assurance Protocols",
"type": "Technical content",
"status": "Done",
"target": "30",
"limit": "33",
"reviewer": "Priya Singh"
},
{
"id": 43,
"header": "Stakeholder Analysis",
"type": "Research",
"status": "In Process",
"target": "11",
"limit": "14",
"reviewer": "Eddie Lake"
},
{
"id": 44,
"header": "Environmental Impact Assessment",
"type": "Research",
"status": "Done",
"target": "24",
"limit": "27",
"reviewer": "Assign reviewer"
},
{
"id": 45,
"header": "Intellectual Property Rights",
"type": "Legal",
"status": "In Process",
"target": "17",
"limit": "20",
"reviewer": "Sarah Johnson"
},
{
"id": 46,
"header": "Customer Support Framework",
"type": "Narrative",
"status": "Done",
"target": "22",
"limit": "25",
"reviewer": "Jamik Tashpulatov"
},
{
"id": 47,
"header": "Version Control Strategy",
"type": "Technical content",
"status": "In Process",
"target": "15",
"limit": "18",
"reviewer": "Assign reviewer"
},
{
"id": 48,
"header": "Continuous Integration Pipeline",
"type": "Technical content",
"status": "Done",
"target": "26",
"limit": "29",
"reviewer": "Michael Chen"
},
{
"id": 49,
"header": "Regulatory Compliance",
"type": "Legal",
"status": "In Process",
"target": "13",
"limit": "16",
"reviewer": "Assign reviewer"
},
{
"id": 50,
"header": "User Authentication System",
"type": "Technical content",
"status": "Done",
"target": "28",
"limit": "31",
"reviewer": "Eddie Lake"
},
{
"id": 51,
"header": "Data Analytics Framework",
"type": "Technical content",
"status": "In Process",
"target": "21",
"limit": "24",
"reviewer": "Jamik Tashpulatov"
},
{
"id": 52,
"header": "Cloud Infrastructure",
"type": "Technical content",
"status": "Done",
"target": "16",
"limit": "19",
"reviewer": "Assign reviewer"
},
{
"id": 53,
"header": "Network Security Measures",
"type": "Technical content",
"status": "In Process",
"target": "29",
"limit": "32",
"reviewer": "Lisa Wong"
},
{
"id": 54,
"header": "Project Timeline",
"type": "Planning",
"status": "Done",
"target": "14",
"limit": "17",
"reviewer": "Eddie Lake"
},
{
"id": 55,
"header": "Resource Allocation",
"type": "Planning",
"status": "In Process",
"target": "27",
"limit": "30",
"reviewer": "Assign reviewer"
},
{
"id": 56,
"header": "Team Structure and Roles",
"type": "Planning",
"status": "Done",
"target": "20",
"limit": "23",
"reviewer": "Jamik Tashpulatov"
},
{
"id": 57,
"header": "Communication Protocols",
"type": "Planning",
"status": "In Process",
"target": "15",
"limit": "18",
"reviewer": "Assign reviewer"
},
{
"id": 58,
"header": "Success Metrics",
"type": "Planning",
"status": "Done",
"target": "30",
"limit": "33",
"reviewer": "Eddie Lake"
},
{
"id": 59,
"header": "Internationalization Support",
"type": "Technical content",
"status": "In Process",
"target": "23",
"limit": "26",
"reviewer": "Jamik Tashpulatov"
},
{
"id": 60,
"header": "Backup and Recovery Procedures",
"type": "Technical content",
"status": "Done",
"target": "18",
"limit": "21",
"reviewer": "Assign reviewer"
},
{
"id": 61,
"header": "Monitoring and Alerting System",
"type": "Technical content",
"status": "In Process",
"target": "25",
"limit": "28",
"reviewer": "Daniel Park"
},
{
"id": 62,
"header": "Code Review Guidelines",
"type": "Technical content",
"status": "Done",
"target": "12",
"limit": "15",
"reviewer": "Eddie Lake"
},
{
"id": 63,
"header": "Documentation Standards",
"type": "Technical content",
"status": "In Process",
"target": "27",
"limit": "30",
"reviewer": "Jamik Tashpulatov"
},
{
"id": 64,
"header": "Release Management Process",
"type": "Planning",
"status": "Done",
"target": "22",
"limit": "25",
"reviewer": "Assign reviewer"
},
{
"id": 65,
"header": "Feature Prioritization Matrix",
"type": "Planning",
"status": "In Process",
"target": "19",
"limit": "22",
"reviewer": "Emma Davis"
},
{
"id": 66,
"header": "Technical Debt Assessment",
"type": "Technical content",
"status": "Done",
"target": "24",
"limit": "27",
"reviewer": "Eddie Lake"
},
{
"id": 67,
"header": "Capacity Planning",
"type": "Planning",
"status": "In Process",
"target": "21",
"limit": "24",
"reviewer": "Jamik Tashpulatov"
},
{
"id": 68,
"header": "Service Level Agreements",
"type": "Legal",
"status": "Done",
"target": "26",
"limit": "29",
"reviewer": "Assign reviewer"
}
]

View File

@ -0,0 +1,35 @@
import { DashboardHeader } from '@/components/dashboard/dashboard-header';
import { getTranslations } from 'next-intl/server';
interface UsersLayoutProps {
children: React.ReactNode;
}
export default async function UsersLayout({ children }: UsersLayoutProps) {
const t = await getTranslations('Dashboard.admin');
const breadcrumbs = [
{
label: t('title'),
isCurrentPage: false,
},
{
label: t('users.title'),
isCurrentPage: true,
},
];
return (
<>
<DashboardHeader breadcrumbs={breadcrumbs} />
<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">
{children}
</div>
</div>
</div>
</>
);
}

View File

@ -0,0 +1,5 @@
import { Loader2Icon } from 'lucide-react';
export default function Loading() {
return <Loader2Icon className="my-32 mx-auto size-6 animate-spin" />;
}

View File

@ -1,302 +1,11 @@
import { UsersTable } from '@/components/admin/users-table';
import { DashboardHeader } from '@/components/dashboard/dashboard-header';
import { useTranslations } from 'next-intl';
// 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,
},
];
import { UsersPageClient } from '@/components/admin/users-page';
/**
* Admin users page
* Users page
*
* NOTICE: This is a demo page for the admin, no real data is used,
* we will show real data in the future
* This page is used to manage users for the admin,
* it is protected and only accessible to the admin role
*/
export default function AdminUsersPage() {
const t = useTranslations();
const breadcrumbs = [
{
label: t('Dashboard.admin.title'),
isCurrentPage: false,
},
{
label: t('Dashboard.admin.users.title'),
isCurrentPage: true,
},
];
return (
<>
<DashboardHeader breadcrumbs={breadcrumbs} />
<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">
<UsersTable data={mockUsers} />
</div>
</div>
</div>
</>
);
export default function UsersPage() {
return <UsersPageClient />;
}

View File

@ -0,0 +1,62 @@
'use client';
import { getUsersAction } from '@/actions/get-users';
import type { User } from '@/components/admin/users-table';
import { UsersTable } from '@/components/admin/users-table';
import { useTranslations } from 'next-intl';
import { useEffect, useState } from 'react';
import { toast } from 'sonner';
export function UsersPageClient() {
const t = useTranslations();
const [pageIndex, setPageIndex] = useState(0);
const [pageSize, setPageSize] = useState(10);
const [search, setSearch] = useState('');
const [data, setData] = useState<User[]>([]);
const [total, setTotal] = useState(0);
const [loading, setLoading] = useState(false);
useEffect(() => {
const fetchUsers = async () => {
try {
setLoading(true);
const result = await getUsersAction({ pageIndex, pageSize, search });
if (result?.data?.success) {
setData(result.data.data?.items || []);
setTotal(result.data.data?.total || 0);
} else {
const errorMessage = result?.data?.error || 'Failed to fetch users';
toast.error(errorMessage);
setData([]);
setTotal(0);
}
} catch (error) {
console.error('Failed to fetch users:', error);
toast.error('An unexpected error occurred while fetching users');
setData([]);
setTotal(0);
} finally {
setLoading(false);
}
};
fetchUsers();
}, [pageIndex, pageSize, search]);
return (
<>
<UsersTable
data={data}
total={total}
pageIndex={pageIndex}
pageSize={pageSize}
onPageChange={setPageIndex}
onPageSizeChange={setPageSize}
onSearch={setSearch}
search={search}
loading={loading}
/>
</>
);
}

View File

@ -50,7 +50,7 @@ import { format } from 'date-fns';
import { useState } from 'react';
import { Label } from '../ui/label';
interface User {
export interface User {
id: string;
name: string;
email: string;
@ -153,37 +153,60 @@ const columns: ColumnDef<User>[] = [
interface UsersTableProps {
data: User[];
total: number;
pageIndex: number;
pageSize: number;
onPageChange: (page: number) => void;
onPageSizeChange: (size: number) => void;
onSearch: (search: string) => void;
search: string;
loading?: boolean;
}
/**
* https://ui.shadcn.com/docs/components/data-table
*/
export function UsersTable({ data }: UsersTableProps) {
export function UsersTable({
data,
total,
pageIndex,
pageSize,
onPageChange,
onPageSizeChange,
onSearch,
search,
loading,
}: 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,
pageCount: Math.ceil(total / pageSize),
state: {
sorting,
columnFilters,
columnVisibility,
globalFilter,
pagination,
pagination: { pageIndex, pageSize },
},
onSortingChange: setSorting,
onColumnFiltersChange: setColumnFilters,
onColumnVisibilityChange: setColumnVisibility,
onGlobalFilterChange: setGlobalFilter,
onPaginationChange: setPagination,
onPaginationChange: (updater) => {
const next =
typeof updater === 'function'
? updater({ pageIndex, pageSize })
: updater;
if (next.pageIndex !== pageIndex) onPageChange(next.pageIndex);
if (next.pageSize !== pageSize) onPageSizeChange(next.pageSize);
},
getCoreRowModel: getCoreRowModel(),
getFilteredRowModel: getFilteredRowModel(),
getPaginationRowModel: getPaginationRowModel(),
getSortedRowModel: getSortedRowModel(),
manualPagination: true,
});
return (
@ -191,8 +214,11 @@ export function UsersTable({ data }: UsersTableProps) {
<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)}
value={search}
onChange={(event) => {
onSearch(event.target.value);
onPageChange(0);
}}
className="max-w-sm"
/>
<DropdownMenu>
@ -247,7 +273,16 @@ export function UsersTable({ data }: UsersTableProps) {
))}
</TableHeader>
<TableBody>
{table.getRowModel().rows?.length ? (
{loading ? (
<TableRow>
<TableCell
colSpan={columns.length}
className="h-24 text-center"
>
Loading...
</TableCell>
</TableRow>
) : table.getRowModel().rows?.length ? (
table.getRowModel().rows.map((row) => (
<TableRow
key={row.id}
@ -286,9 +321,10 @@ export function UsersTable({ data }: UsersTableProps) {
Rows per page
</Label>
<Select
value={`${table.getState().pagination.pageSize}`}
value={`${pageSize}`}
onValueChange={(value) => {
table.setPageSize(Number(value));
onPageSizeChange(Number(value));
onPageChange(0);
}}
>
<SelectTrigger
@ -296,9 +332,7 @@ export function UsersTable({ data }: UsersTableProps) {
className="w-20 cursor-pointer"
id="rows-per-page"
>
<SelectValue
placeholder={table.getState().pagination.pageSize}
/>
<SelectValue placeholder={pageSize} />
</SelectTrigger>
<SelectContent side="top">
{[10, 20, 30, 40, 50].map((pageSize) => (
@ -310,15 +344,14 @@ export function UsersTable({ data }: UsersTableProps) {
</Select>
</div>
<div className="flex w-fit items-center justify-center text-sm font-medium">
Page {table.getState().pagination.pageIndex + 1} of{' '}
{table.getPageCount()}
Page {pageIndex + 1} of {Math.max(1, Math.ceil(total / pageSize))}
</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()}
onClick={() => onPageChange(0)}
disabled={pageIndex === 0}
>
<span className="sr-only">Go to first page</span>
<IconChevronsLeft />
@ -327,8 +360,8 @@ export function UsersTable({ data }: UsersTableProps) {
variant="outline"
className="cursor-pointer size-8"
size="icon"
onClick={() => table.previousPage()}
disabled={!table.getCanPreviousPage()}
onClick={() => onPageChange(pageIndex - 1)}
disabled={pageIndex === 0}
>
<span className="sr-only">Go to previous page</span>
<IconChevronLeft />
@ -337,8 +370,8 @@ export function UsersTable({ data }: UsersTableProps) {
variant="outline"
className="cursor-pointer size-8"
size="icon"
onClick={() => table.nextPage()}
disabled={!table.getCanNextPage()}
onClick={() => onPageChange(pageIndex + 1)}
disabled={pageIndex + 1 >= Math.ceil(total / pageSize)}
>
<span className="sr-only">Go to next page</span>
<IconChevronRight />
@ -347,8 +380,10 @@ export function UsersTable({ data }: UsersTableProps) {
variant="outline"
className="cursor-pointer hidden size-8 lg:flex"
size="icon"
onClick={() => table.setPageIndex(table.getPageCount() - 1)}
disabled={!table.getCanNextPage()}
onClick={() =>
onPageChange(Math.max(0, Math.ceil(total / pageSize) - 1))
}
disabled={pageIndex + 1 >= Math.ceil(total / pageSize)}
>
<span className="sr-only">Go to last page</span>
<IconChevronsRight />