All files / src/components/admin UserTable.tsx

100% Statements 7/7
100% Branches 10/10
100% Functions 6/6
100% Lines 7/7

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130                                                                  253x   253x                     2x             1x                 1x             414x                                                                                     16x                                  
import { Link } from "@tanstack/react-router";
import { Eye, Ban, CheckCircle } from "lucide-react";
import { Button } from "../ui/button";
import type { AdminUser } from "../../lib/api";
import { useSortable } from "../../hooks/useSortable";
// `SortableTableHead`, not `ui/sortable-header`: the older component renders
// its own bare `<th className="pb-3">`, which loses the Table family's `px-3`
// so the column head stops lining up with the cells under it. See the header
// of `SortableTableHead.tsx` for the two a11y defects it also fixes.
import { SortableTableHead } from "./SortableTableHead";
import {
	Table,
	TableBody,
	TableCaption,
	TableCell,
	TableHead,
	TableHeader,
	TableRow,
} from "../ui/table";
import { StatusBadge } from "../ui/status-badge";
import { formatDate } from "../../lib/utils";
 
interface UserTableProps {
	users: AdminUser[];
	onToggleBan: (user: AdminUser) => void;
}
 
export function UserTable({ users, onToggleBan }: UserTableProps) {
	const {
		sorted: sortedUsers,
		sortKey,
		sortOrder,
		toggleSort,
	} = useSortable(users, "name" as keyof AdminUser, "asc");
 
	return (
		// No wrapper inset: the table is ruled directly onto the card stock.
		<Table density="dense">
			<TableCaption>Platform users, with their account status.</TableCaption>
			<TableHeader>
				<TableRow>
					<SortableTableHead
						label="Name"
						sortKey="name"
						currentSortKey={sortKey as string}
						sortOrder={sortOrder}
						onSort={(key) => toggleSort(key as keyof AdminUser)}
					/>
					<SortableTableHead
						label="Email"
						sortKey="email"
						currentSortKey={sortKey as string}
						sortOrder={sortOrder}
						onSort={(key) => toggleSort(key as keyof AdminUser)}
					/>
					<TableHead>Status</TableHead>
					<TableHead>Phone</TableHead>
					<SortableTableHead
						label="Created"
						sortKey="createdAt"
						currentSortKey={sortKey as string}
						sortOrder={sortOrder}
						onSort={(key) => toggleSort(key as keyof AdminUser)}
					/>
					<TableHead>Actions</TableHead>
				</TableRow>
			</TableHeader>
			<TableBody>
				{sortedUsers.map((user) => (
					<TableRow key={user.id}>
						<TableCell>
							<Link
								to={`/admin/users/${user.id}`}
								className="font-medium text-foreground-default hover:text-primary-600"
							>
								{user.name}
							</Link>
						</TableCell>
						<TableCell className="text-foreground-muted">
							{user.email}
						</TableCell>
						<TableCell>
							{/*
							 * A printed code is TEXT — the Ban/CheckCircle glyphs that used to
							 * sit inside the pill are gone, and the word carries the state, so
							 * colour never travels alone.
							 */}
							{user.banned ? (
								<StatusBadge status="Banned" variant="error" />
							) : user.emailVerified ? (
								<StatusBadge status="Active" variant="success" />
							) : (
								<StatusBadge status="Unverified" variant="warning" />
							)}
						</TableCell>
						<TableCell className="text-foreground-muted">
							{user.phoneNumber || "-"}
						</TableCell>
						{/* `en-IN` from one place — @interioring/utils/format/date. */}
						<TableCell className="whitespace-nowrap tabular-nums text-foreground-muted">
							{formatDate(user.createdAt)}
						</TableCell>
						<TableCell>
							<div className="flex items-center gap-2">
								<Link to={`/admin/users/${user.id}`}>
									<Button variant="ghost" size="sm">
										<Eye className="h-4 w-4" />
									</Button>
								</Link>
								<Button
									variant="ghost"
									size="sm"
									onClick={() => onToggleBan(user)}
									title={user.banned ? "Unban user" : "Ban user"}
								>
									{user.banned ? (
										<CheckCircle className="h-4 w-4 text-success" />
									) : (
										<Ban className="h-4 w-4 text-error" />
									)}
								</Button>
							</div>
						</TableCell>
					</TableRow>
				))}
			</TableBody>
		</Table>
	);
}