mirror of
https://github.com/hanzo-apps/dataroom.git
synced 2026-10-30 14:47:36 +00:00
337 lines
9.1 KiB
TypeScript
337 lines
9.1 KiB
TypeScript
import { useRouter } from "next/router";
|
|
|
|
import { useCallback, useMemo } from "react";
|
|
import React from "react";
|
|
|
|
import {
|
|
ColumnDef,
|
|
flexRender,
|
|
getCoreRowModel,
|
|
useReactTable,
|
|
} from "@tanstack/react-table";
|
|
import {
|
|
ChevronDownIcon,
|
|
ChevronUpIcon,
|
|
ChevronsUpDownIcon,
|
|
} from "lucide-react";
|
|
|
|
import { timeAgo } from "@/lib/utils";
|
|
|
|
import { Pagination } from "@/components/documents/pagination";
|
|
import { Button } from "@/components/ui/button";
|
|
import {
|
|
Table,
|
|
TableBody,
|
|
TableCell,
|
|
TableHead,
|
|
TableHeader,
|
|
TableRow,
|
|
} from "@/components/ui/table";
|
|
import { TimestampTooltip } from "@/components/ui/timestamp-tooltip";
|
|
import { VisitorAvatar } from "@/components/visitors/visitor-avatar";
|
|
|
|
import { Skeleton } from "../ui/skeleton";
|
|
|
|
type Viewer = {
|
|
id: string;
|
|
email: string;
|
|
createdAt: Date;
|
|
updatedAt: Date;
|
|
totalVisits: number;
|
|
lastViewed: Date | null;
|
|
viewerName?: string | null;
|
|
};
|
|
|
|
export function ContactsTable({
|
|
viewers,
|
|
pagination,
|
|
sorting,
|
|
onPageChange,
|
|
onPageSizeChange,
|
|
onSortChange,
|
|
}: {
|
|
viewers: Viewer[] | null | undefined;
|
|
pagination?: {
|
|
currentPage: number;
|
|
pageSize: number;
|
|
totalItems: number;
|
|
totalPages: number;
|
|
hasNext: boolean;
|
|
hasPrev: boolean;
|
|
};
|
|
sorting?: {
|
|
sortBy: string;
|
|
sortOrder: string;
|
|
};
|
|
onPageChange?: (page: number) => void;
|
|
onPageSizeChange?: (size: number) => void;
|
|
onSortChange?: (sortBy: string, sortOrder: string) => void;
|
|
}) {
|
|
const router = useRouter();
|
|
|
|
const data = useMemo(() => viewers || [], [viewers]);
|
|
|
|
const handlePageChange = (page: number) => {
|
|
if (onPageChange) {
|
|
onPageChange(page);
|
|
}
|
|
};
|
|
|
|
const handlePageSizeChange = (size: number) => {
|
|
if (onPageSizeChange) {
|
|
onPageSizeChange(size);
|
|
}
|
|
};
|
|
|
|
const handleSort = useCallback(
|
|
(columnId: string) => {
|
|
if (!onSortChange) return;
|
|
|
|
const currentSortBy = sorting?.sortBy;
|
|
const currentSortOrder = sorting?.sortOrder;
|
|
|
|
if (currentSortBy === columnId) {
|
|
if (columnId === "lastViewed") {
|
|
if (currentSortOrder === "asc") {
|
|
onSortChange("lastViewed", "desc");
|
|
} else {
|
|
onSortChange("lastViewed", "asc");
|
|
}
|
|
} else {
|
|
if (currentSortOrder === "asc") {
|
|
onSortChange(columnId, "desc");
|
|
} else if (currentSortOrder === "desc") {
|
|
onSortChange("lastViewed", "desc");
|
|
}
|
|
}
|
|
} else {
|
|
onSortChange(columnId, "asc");
|
|
}
|
|
},
|
|
[onSortChange, sorting?.sortBy, sorting?.sortOrder],
|
|
);
|
|
|
|
const getSortIcon = useCallback(
|
|
(columnId: string) => {
|
|
const currentSortBy = sorting?.sortBy;
|
|
const currentSortOrder = sorting?.sortOrder;
|
|
|
|
if (currentSortBy !== columnId) {
|
|
return <ChevronsUpDownIcon className="ml-2 h-4 w-4" />;
|
|
}
|
|
|
|
return currentSortOrder === "asc" ? (
|
|
<ChevronUpIcon className="ml-2 h-4 w-4" />
|
|
) : (
|
|
<ChevronDownIcon className="ml-2 h-4 w-4" />
|
|
);
|
|
},
|
|
[sorting?.sortBy, sorting?.sortOrder],
|
|
);
|
|
|
|
const getSortClass = useCallback(
|
|
(columnId: string) => {
|
|
const currentSortBy = sorting?.sortBy;
|
|
return currentSortBy === columnId
|
|
? "text-nowrap font-medium"
|
|
: "text-nowrap font-normal";
|
|
},
|
|
[sorting?.sortBy],
|
|
);
|
|
|
|
const columns: ColumnDef<Viewer>[] = useMemo(
|
|
() => [
|
|
{
|
|
accessorKey: "email",
|
|
header: "Contact",
|
|
cell: ({ row }) => (
|
|
<div className="flex items-center overflow-visible sm:space-x-3">
|
|
<VisitorAvatar viewerEmail={row.original.email} />
|
|
<div className="min-w-0 flex-1">
|
|
<div className="focus:outline-none">
|
|
<p className="flex items-center gap-x-2 overflow-visible text-sm font-medium text-gray-800 dark:text-gray-200">
|
|
{row.original.viewerName || row.original.email}
|
|
</p>
|
|
{row.original.viewerName && row.original.email && (
|
|
<p className="text-xs text-muted-foreground/60">
|
|
{row.original.email}
|
|
</p>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
),
|
|
},
|
|
{
|
|
accessorKey: "lastViewed",
|
|
header: ({ column }) => {
|
|
return (
|
|
<Button
|
|
variant="ghost"
|
|
onClick={() => handleSort("lastViewed")}
|
|
className={getSortClass("lastViewed")}
|
|
>
|
|
Last Viewed
|
|
{getSortIcon("lastViewed")}
|
|
</Button>
|
|
);
|
|
},
|
|
cell: ({ row }) => {
|
|
const lastView = row.original.lastViewed;
|
|
return lastView ? (
|
|
<TimestampTooltip
|
|
timestamp={lastView}
|
|
side="right"
|
|
rows={["local", "utc", "unix"]}
|
|
>
|
|
<time
|
|
className="select-none text-sm text-muted-foreground"
|
|
dateTime={new Date(lastView).toISOString()}
|
|
>
|
|
{timeAgo(lastView)}
|
|
</time>
|
|
</TimestampTooltip>
|
|
) : (
|
|
<div className="text-sm text-muted-foreground">-</div>
|
|
);
|
|
},
|
|
},
|
|
{
|
|
accessorKey: "totalVisits",
|
|
header: ({ column }) => {
|
|
return (
|
|
<Button
|
|
variant="ghost"
|
|
onClick={() => handleSort("totalVisits")}
|
|
className={getSortClass("totalVisits")}
|
|
>
|
|
Total Views
|
|
{getSortIcon("totalVisits")}
|
|
</Button>
|
|
);
|
|
},
|
|
cell: ({ row }) => (
|
|
<div className="text-sm text-muted-foreground">
|
|
{row.original.totalVisits}
|
|
</div>
|
|
),
|
|
},
|
|
],
|
|
[handleSort, getSortIcon, getSortClass],
|
|
);
|
|
|
|
const table = useReactTable({
|
|
data,
|
|
columns,
|
|
getCoreRowModel: getCoreRowModel(),
|
|
manualPagination: true,
|
|
});
|
|
|
|
if (!viewers) {
|
|
return (
|
|
<div className="rounded-md border">
|
|
<Table>
|
|
<TableHeader>
|
|
<TableRow>
|
|
<TableHead>Name</TableHead>
|
|
<TableHead>Last Viewed</TableHead>
|
|
<TableHead>Total Views</TableHead>
|
|
</TableRow>
|
|
</TableHeader>
|
|
<TableBody>
|
|
{[...Array(5)].map((_, index) => (
|
|
<TableRow key={index}>
|
|
<TableCell>
|
|
<div className="flex items-center space-x-3">
|
|
<Skeleton className="h-10 w-10 rounded-full" />
|
|
<Skeleton className="h-4 w-[200px]" />
|
|
</div>
|
|
</TableCell>
|
|
<TableCell>
|
|
<Skeleton className="h-4 w-[100px]" />
|
|
</TableCell>
|
|
<TableCell>
|
|
<Skeleton className="h-4 w-[50px]" />
|
|
</TableCell>
|
|
</TableRow>
|
|
))}
|
|
</TableBody>
|
|
</Table>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
const handleRowClick = (id: string) => {
|
|
router.push(`/visitors/${id}`);
|
|
};
|
|
|
|
return (
|
|
<div className="space-y-4">
|
|
<div className="rounded-md border">
|
|
<Table>
|
|
<TableHeader>
|
|
{table.getHeaderGroups().map((headerGroup) => (
|
|
<TableRow key={headerGroup.id}>
|
|
{headerGroup.headers.map((header) => (
|
|
<TableHead key={header.id} className="px-0 first:px-4">
|
|
{header.isPlaceholder
|
|
? null
|
|
: flexRender(
|
|
header.column.columnDef.header,
|
|
header.getContext(),
|
|
)}
|
|
</TableHead>
|
|
))}
|
|
</TableRow>
|
|
))}
|
|
</TableHeader>
|
|
<TableBody>
|
|
{table.getRowModel().rows?.length ? (
|
|
table.getRowModel().rows.map((row) => (
|
|
<TableRow
|
|
key={row.id}
|
|
onClick={() => handleRowClick(row.original.id)}
|
|
className="cursor-pointer"
|
|
>
|
|
{row.getVisibleCells().map((cell) => {
|
|
return (
|
|
<TableCell key={cell.id}>
|
|
{flexRender(
|
|
cell.column.columnDef.cell,
|
|
cell.getContext(),
|
|
)}
|
|
</TableCell>
|
|
);
|
|
})}
|
|
</TableRow>
|
|
))
|
|
) : (
|
|
<TableRow>
|
|
<TableCell
|
|
colSpan={columns.length}
|
|
className="h-24 text-center"
|
|
>
|
|
No visitors yet.
|
|
</TableCell>
|
|
</TableRow>
|
|
)}
|
|
</TableBody>
|
|
</Table>
|
|
</div>
|
|
|
|
{pagination && pagination.totalItems > 0 && (
|
|
<Pagination
|
|
currentPage={pagination.currentPage}
|
|
pageSize={pagination.pageSize}
|
|
totalItems={pagination.totalItems}
|
|
totalPages={pagination.totalPages}
|
|
onPageChange={handlePageChange}
|
|
onPageSizeChange={handlePageSizeChange}
|
|
totalShownItems={data.length}
|
|
itemName="visitors"
|
|
/>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|