"use client"; import { useTransition, useCallback, useRef } from "react"; import { useRouter, useSearchParams } from "next/navigation"; import { format } from "date-fns"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from "ui/table"; import { Badge } from "ui/badge"; import { Input } from "ui/input"; import { buttonVariants } from "ui/button"; import { Avatar, AvatarFallback, AvatarImage } from "ui/avatar"; import { Search, ChevronRight, X } from "lucide-react"; import { AdminUserListItem } from "app-types/admin"; import { cn } from "lib/utils"; import { useDebounce } from "@/hooks/use-debounce"; import { TablePagination } from "ui/table-pagination"; import Form from "next/form"; import Link from "next/link"; import { SortableHeader } from "ui/sortable-header"; import { getUserAvatar } from "lib/user/utils"; import { useTranslations } from "next-intl"; import { UserRoleBadges } from "@/components/user/user-detail/user-role-badges"; import { UserStatusBadge } from "@/components/user/user-detail/user-status-badge"; import { buildUserDetailUrl } from "@/lib/admin/navigation-utils"; const DEFAULT_SORT_BY = "createdAt"; const DEFAULT_SORT_DIRECTION = "desc"; interface UsersTableProps { users: AdminUserListItem[]; currentUserId: string; total: number; page: number; limit: number; query?: string; baseUrl?: string; sortBy: string; sortDirection: "asc" | "desc"; } export function UsersTable({ users, currentUserId, total, page, limit, query, baseUrl = "/admin/users", sortBy = DEFAULT_SORT_BY, sortDirection = DEFAULT_SORT_DIRECTION, }: UsersTableProps) { const router = useRouter(); const searchParams = useSearchParams(); const [_, startTransition] = useTransition(); const formRef = useRef(null); const inputRef = useRef(null); const t = useTranslations("Admin.Users"); const shouldAutoFocusRef = useRef(false); const submitForm = useCallback(() => { // Track that we're about to submit and should maintain focus formRef.current?.requestSubmit(); }, []); const debouncedSetUrlQuery = useDebounce(submitForm, 300); const totalPages = Math.ceil(total / limit); const buildUrl = useCallback( ( params: { page?: number; sortBy?: string; sortDirection?: "asc" | "desc"; query?: string; } = {}, ) => { const searchParams = new URLSearchParams(); // Use provided values or fall back to current values const finalPage = params.page ?? page; const finalSortBy = params.sortBy ?? sortBy; const finalSortDirection = params.sortDirection ?? sortDirection; const finalQuery = params.query ?? query; // Only add non-default values to keep URLs clean if (finalPage && finalPage !== 1) { searchParams.set("page", finalPage.toString()); } if (finalSortBy && finalSortBy !== DEFAULT_SORT_BY) { searchParams.set("sortBy", finalSortBy); } if (finalSortDirection && finalSortDirection !== DEFAULT_SORT_DIRECTION) { searchParams.set("sortDirection", finalSortDirection); } if (finalQuery) { searchParams.set("query", finalQuery); } const queryString = searchParams.toString(); return queryString ? `${baseUrl}?${queryString}` : baseUrl; }, [baseUrl, page, sortBy, sortDirection, query], ); const handleSort = useCallback( (field: string) => { const newSortDirection = sortBy === field && sortDirection === "asc" ? "desc" : "asc"; router.push( buildUrl({ sortBy: field, sortDirection: newSortDirection, page: 1, }), ); }, [sortBy, sortDirection, router, buildUrl], ); const handleRowClick = (userId: string) => { startTransition(() => { // Get current search params as string const currentSearchString = searchParams.toString(); const url = buildUserDetailUrl(userId, currentSearchString); router.push(url); }); }; return (
{page !== 1 && } {sortBy !== DEFAULT_SORT_BY && ( )} {sortDirection !== DEFAULT_SORT_DIRECTION && ( )} { shouldAutoFocusRef.current = true; }} onChange={() => { debouncedSetUrlQuery(); }} data-testid="users-search-input" />
{(query || sortBy !== DEFAULT_SORT_BY || sortDirection !== DEFAULT_SORT_DIRECTION) && ( {t("clear")} )}
{t("totalCount", { count: total })}
{t("user")} {t("role")} {t("status")} {t("joined")} {users.length === 0 ? ( {t("noUsersFound")} ) : ( users.map((user) => ( handleRowClick(user.id)} data-testid={`user-row-${user.id}`} >
{user.name.slice(0, 2).toUpperCase()}
{user.name} {user.id === currentUserId && ( {t("youBadge")} )}
{user.email}
{format(new Date(user.createdAt), "MMM d, yyyy")}
)) )}
); }