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>
);
}
|