feat: add user management components including header, loading states, and table with pagination

This commit is contained in:
JSC
2025-08-17 11:44:08 +02:00
parent 75ecd26e06
commit 46bfcad271
5 changed files with 515 additions and 130 deletions

View File

@@ -0,0 +1,156 @@
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select'
import { Filter, RefreshCw, Search, SortAsc, SortDesc, X } from 'lucide-react'
export type UserSortField = 'name' | 'email' | 'role' | 'credits' | 'created_at'
export type SortOrder = 'asc' | 'desc'
export type UserStatus = 'all' | 'active' | 'inactive'
interface UsersHeaderProps {
searchQuery: string
onSearchChange: (query: string) => void
sortBy: UserSortField
onSortByChange: (sortBy: UserSortField) => void
sortOrder: SortOrder
onSortOrderChange: (order: SortOrder) => void
statusFilter: UserStatus
onStatusFilterChange: (status: UserStatus) => void
onRefresh: () => void
loading: boolean
error: string | null
userCount: number
}
export function UsersHeader({
searchQuery,
onSearchChange,
sortBy,
onSortByChange,
sortOrder,
onSortOrderChange,
statusFilter,
onStatusFilterChange,
onRefresh,
loading,
error,
userCount,
}: UsersHeaderProps) {
return (
<>
{/* Header */}
<div className="flex items-center justify-between mb-6">
<div>
<h1 className="text-2xl font-bold">User Management</h1>
<p className="text-muted-foreground">
Manage user accounts and permissions
</p>
</div>
<div className="flex items-center gap-4">
{!loading && !error && (
<div className="text-sm text-muted-foreground">
{statusFilter !== 'all'
? `${userCount} ${statusFilter} user${userCount !== 1 ? 's' : ''}`
: `${userCount} user${userCount !== 1 ? 's' : ''}`
}
</div>
)}
<Button onClick={onRefresh} disabled={loading}>
<RefreshCw className={`h-4 w-4 mr-2 ${loading ? 'animate-spin' : ''}`} />
Refresh
</Button>
</div>
</div>
{/* Search and Sort Controls */}
<div className="flex flex-col sm:flex-row gap-4 mb-6">
<div className="flex-1">
<div className="relative">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-muted-foreground" />
<Input
placeholder="Search users..."
value={searchQuery}
onChange={e => onSearchChange(e.target.value)}
className="pl-9 pr-9"
/>
{searchQuery && (
<Button
variant="ghost"
size="sm"
onClick={() => onSearchChange('')}
className="absolute right-1 top-1/2 transform -translate-y-1/2 h-7 w-7 p-0 hover:bg-muted"
title="Clear search"
>
<X className="h-3 w-3" />
</Button>
)}
</div>
</div>
<div className="flex gap-2">
<Select
value={sortBy}
onValueChange={value => onSortByChange(value as UserSortField)}
>
<SelectTrigger className="w-[180px]">
<SelectValue placeholder="Sort by" />
</SelectTrigger>
<SelectContent>
<SelectItem value="name">Name</SelectItem>
<SelectItem value="email">Email</SelectItem>
<SelectItem value="role">Role</SelectItem>
<SelectItem value="credits">Credits</SelectItem>
<SelectItem value="created_at">Created Date</SelectItem>
</SelectContent>
</Select>
<Button
variant="outline"
size="icon"
onClick={() => onSortOrderChange(sortOrder === 'asc' ? 'desc' : 'asc')}
title={sortOrder === 'asc' ? 'Sort ascending' : 'Sort descending'}
>
{sortOrder === 'asc' ? (
<SortAsc className="h-4 w-4" />
) : (
<SortDesc className="h-4 w-4" />
)}
</Button>
<Select
value={statusFilter}
onValueChange={value => onStatusFilterChange(value as UserStatus)}
>
<SelectTrigger className="w-[120px]">
<Filter className="h-4 w-4 mr-2" />
<SelectValue placeholder="Status" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">All Status</SelectItem>
<SelectItem value="active">Active</SelectItem>
<SelectItem value="inactive">Inactive</SelectItem>
</SelectContent>
</Select>
<Button
variant="outline"
size="icon"
onClick={onRefresh}
disabled={loading}
title="Refresh users"
>
<RefreshCw
className={`h-4 w-4 ${loading ? 'animate-spin' : ''}`}
/>
</Button>
</div>
</div>
</>
)
}

View File

@@ -0,0 +1,112 @@
import { Button } from '@/components/ui/button'
import { Skeleton } from '@/components/ui/skeleton'
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from '@/components/ui/table'
import { AlertCircle, RefreshCw, Users } from 'lucide-react'
export function UsersLoading() {
return (
<div className="space-y-4">
<div className="rounded-md border">
<Table>
<TableHeader>
<TableRow>
<TableHead>Name</TableHead>
<TableHead>Email</TableHead>
<TableHead>Role</TableHead>
<TableHead>Plan</TableHead>
<TableHead>Credits</TableHead>
<TableHead>Status</TableHead>
<TableHead>Actions</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{Array.from({ length: 10 }).map((_, i) => (
<TableRow key={i}>
<TableCell>
<Skeleton className="h-4 w-24" />
</TableCell>
<TableCell>
<Skeleton className="h-4 w-32" />
</TableCell>
<TableCell>
<Skeleton className="h-5 w-16 rounded-full" />
</TableCell>
<TableCell>
<Skeleton className="h-4 w-20" />
</TableCell>
<TableCell>
<Skeleton className="h-4 w-16" />
</TableCell>
<TableCell>
<Skeleton className="h-5 w-16 rounded-full" />
</TableCell>
<TableCell>
<div className="flex items-center gap-2">
<Skeleton className="h-8 w-8" />
<Skeleton className="h-8 w-8" />
</div>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
</div>
)
}
export function UsersError({ error, onRetry }: { error: string; onRetry: () => void }) {
return (
<div className="flex flex-col items-center justify-center py-16 space-y-4">
<div className="rounded-full p-3 bg-destructive/10">
<AlertCircle className="h-8 w-8 text-destructive" />
</div>
<div className="text-center space-y-2">
<h3 className="text-lg font-semibold">Failed to load users</h3>
<p className="text-muted-foreground max-w-md">
{error}
</p>
</div>
<Button onClick={onRetry} variant="outline">
<RefreshCw className="h-4 w-4 mr-2" />
Try again
</Button>
</div>
)
}
export function UsersEmpty({ searchQuery, statusFilter }: {
searchQuery: string;
statusFilter: string;
}) {
return (
<div className="flex flex-col items-center justify-center py-16 space-y-4">
<div className="rounded-full p-3 bg-muted">
<Users className="h-8 w-8 text-muted-foreground" />
</div>
<div className="text-center space-y-2">
<h3 className="text-lg font-semibold">
{searchQuery || statusFilter !== 'all'
? 'No users found'
: 'No users yet'
}
</h3>
<p className="text-muted-foreground max-w-md">
{searchQuery
? `No users match "${searchQuery}"`
: statusFilter !== 'all'
? `No ${statusFilter} users found`
: 'Users will appear here once they are added to the system'
}
</p>
</div>
</div>
)
}

View File

@@ -0,0 +1,88 @@
import { Badge } from '@/components/ui/badge'
import { Button } from '@/components/ui/button'
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from '@/components/ui/table'
import type { User } from '@/types/auth'
import { Edit, UserCheck, UserX } from 'lucide-react'
interface UsersTableProps {
users: User[]
onEdit: (user: User) => void
onToggleStatus: (user: User) => void
}
export function UsersTable({ users, onEdit, onToggleStatus }: UsersTableProps) {
const getRoleBadge = (role: string) => {
return (
<Badge variant={role === 'admin' ? 'destructive' : 'secondary'}>
{role}
</Badge>
)
}
const getStatusBadge = (isActive: boolean) => {
return (
<Badge variant={isActive ? 'default' : 'secondary'}>
{isActive ? 'Active' : 'Inactive'}
</Badge>
)
}
return (
<div className="rounded-md border">
<Table>
<TableHeader>
<TableRow>
<TableHead>Name</TableHead>
<TableHead>Email</TableHead>
<TableHead>Role</TableHead>
<TableHead>Plan</TableHead>
<TableHead>Credits</TableHead>
<TableHead>Status</TableHead>
<TableHead>Actions</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{users.map(user => (
<TableRow key={user.id}>
<TableCell className="font-medium">{user.name}</TableCell>
<TableCell>{user.email}</TableCell>
<TableCell>{getRoleBadge(user.role)}</TableCell>
<TableCell>{user.plan.name}</TableCell>
<TableCell>{user.credits.toLocaleString()}</TableCell>
<TableCell>{getStatusBadge(user.is_active)}</TableCell>
<TableCell>
<div className="flex items-center gap-2">
<Button
variant="ghost"
size="sm"
onClick={() => onEdit(user)}
>
<Edit className="h-4 w-4" />
</Button>
<Button
variant="ghost"
size="sm"
onClick={() => onToggleStatus(user)}
>
{user.is_active ? (
<UserX className="h-4 w-4" />
) : (
<UserCheck className="h-4 w-4" />
)}
</Button>
</div>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
)
}