feat: get users and show users in table
This commit is contained in:
parent
4dd36ed9c4
commit
08a3fbf704
57
src/actions/get-users.ts
Normal file
57
src/actions/get-users.ts
Normal 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',
|
||||
};
|
||||
}
|
||||
});
|
@ -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"
|
||||
}
|
||||
]
|
35
src/app/[locale]/(protected)/admin/users/layout.tsx
Normal file
35
src/app/[locale]/(protected)/admin/users/layout.tsx
Normal 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>
|
||||
</>
|
||||
);
|
||||
}
|
5
src/app/[locale]/(protected)/admin/users/loading.tsx
Normal file
5
src/app/[locale]/(protected)/admin/users/loading.tsx
Normal 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" />;
|
||||
}
|
@ -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 />;
|
||||
}
|
||||
|
62
src/components/admin/users-page.tsx
Normal file
62
src/components/admin/users-page.tsx
Normal 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}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
@ -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 />
|
||||
|
Loading…
Reference in New Issue
Block a user