Files
dataroom/components/visitors/dataroom-visitors-history.tsx
Marc Seitz f4e47d8cdc feat: add timestamp tooltip
thanks to dub for the incredible work!
2025-11-06 18:54:58 +04:00

369 lines
13 KiB
TypeScript

import Link from "next/link";
import {
DownloadCloudIcon,
FileCheckIcon,
FileIcon,
UploadCloudIcon,
} from "lucide-react";
import { useDataroomVisitHistory } from "@/lib/swr/use-dataroom";
import { timeAgo } from "@/lib/utils";
import { Button } from "@/components/ui/button";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover";
import { ScrollArea } from "@/components/ui/scroll-area";
import { Skeleton } from "@/components/ui/skeleton";
import { TableCell, TableRow } from "@/components/ui/table";
import { TimestampTooltip } from "@/components/ui/timestamp-tooltip";
export default function DataroomVisitHistory({
viewId,
dataroomId,
}: {
viewId: string;
dataroomId: string;
}) {
const { documentViews, uploadedDocumentViews } = useDataroomVisitHistory({
viewId,
dataroomId,
});
// Group bulk and folder downloads together
const groupedViews = documentViews
? documentViews.reduce(
(acc, view) => {
if (view.downloadType === "BULK" || view.downloadType === "FOLDER") {
// Group bulk/folder downloads by type and timestamp
const key = `${view.downloadType}-${new Date(view.viewedAt).toISOString()}`;
if (!acc.bulkDownloads[key]) {
acc.bulkDownloads[key] = {
type: view.downloadType,
viewedAt: view.viewedAt,
downloadedAt: view.downloadedAt,
metadata: view.downloadMetadata,
documents: [],
};
}
acc.bulkDownloads[key].documents.push(view);
} else {
// Keep individual views
acc.individualViews.push(view);
}
return acc;
},
{
individualViews: [] as typeof documentViews,
bulkDownloads: {} as Record<
string,
{
type: string;
viewedAt: string;
downloadedAt: string;
metadata?: {
folderName?: string;
folderPath?: string;
dataroomName?: string;
documentCount?: number;
documents?: {
id: string;
name: string;
}[];
};
documents: typeof documentViews;
}
>,
},
)
: { individualViews: [], bulkDownloads: {} };
// Combine and sort all events chronologically (oldest to newest)
const allEvents: Array<{
type: "upload" | "view" | "download" | "bulk-download";
timestamp: Date;
data: any;
}> = [];
// Add uploads
uploadedDocumentViews?.forEach((upload) => {
allEvents.push({
type: "upload",
timestamp: new Date(upload.uploadedAt),
data: upload,
});
});
// Add bulk/folder downloads
Object.entries(groupedViews.bulkDownloads).forEach(([key, bulkGroup]) => {
allEvents.push({
type: "bulk-download",
timestamp: new Date(bulkGroup.downloadedAt),
data: bulkGroup,
});
});
// Add individual views
groupedViews.individualViews.forEach((view) => {
const viewedAtTime = new Date(view.viewedAt).getTime();
const downloadedAtTime = view.downloadedAt
? new Date(view.downloadedAt).getTime()
: null;
const isDownloadOnly =
downloadedAtTime && Math.abs(viewedAtTime - downloadedAtTime) < 1000;
// Add view event (if not download-only)
if (!isDownloadOnly) {
allEvents.push({
type: "view",
timestamp: new Date(view.viewedAt),
data: view,
});
}
// Add download event (if exists)
if (view.downloadedAt) {
allEvents.push({
type: "download",
timestamp: new Date(view.downloadedAt),
data: view,
});
}
});
// Sort chronologically (oldest to newest)
allEvents.sort((a, b) => a.timestamp.getTime() - b.timestamp.getTime());
return (
<>
{allEvents.length > 0 ? (
allEvents.map((event, index) => {
if (event.type === "upload") {
const upload = event.data;
return (
<TableRow key={`upload-${upload.documentId}-${index}`}>
<TableCell>
<div className="flex items-center gap-x-4 overflow-visible">
<UploadCloudIcon className="h-5 w-5 text-[#fb7a00]" />
Uploaded {upload.originalFilename}
</div>
</TableCell>
<TableCell>
<TimestampTooltip
timestamp={event.timestamp}
side="right"
rows={["local", "utc", "unix"]}
>
<time
className="select-none truncate text-sm text-muted-foreground"
dateTime={event.timestamp.toISOString()}
>
{timeAgo(event.timestamp)}
</time>
</TimestampTooltip>
</TableCell>
<TableCell className="table-cell">
<div className="flex items-center justify-end space-x-4">
<Button size={"sm"} variant={"link"}>
<Link href={`/documents/${upload.documentId}`}>
See document
</Link>
</Button>
</div>
</TableCell>
</TableRow>
);
}
if (event.type === "bulk-download") {
const bulkGroup = event.data;
const documentCount =
bulkGroup.metadata?.documentCount || bulkGroup.documents.length;
const hasDocumentList =
bulkGroup.metadata?.documents &&
bulkGroup.metadata.documents.length > 0;
return (
<TableRow key={`bulk-${index}`}>
<TableCell>
<div className="flex items-center gap-x-4 overflow-visible">
<DownloadCloudIcon className="h-5 w-5 text-[#fb7a00]" />
<span>
Downloaded{" "}
{hasDocumentList ? (
<Popover>
<PopoverTrigger asChild>
<button className="underline decoration-dotted hover:text-primary">
{documentCount} document
{documentCount !== 1 ? "s" : ""}
</button>
</PopoverTrigger>
<PopoverContent className="w-80">
<div className="space-y-2">
<h4 className="font-medium leading-none">
{bulkGroup.type === "FOLDER" &&
bulkGroup.metadata?.folderName
? `Files in ${bulkGroup.metadata.folderName}`
: `Files in ${bulkGroup.metadata?.dataroomName || "dataroom"}`}
</h4>
<ScrollArea className="h-[200px] w-full rounded-md border p-2">
<div className="space-y-1">
{bulkGroup.metadata.documents!.map(
(doc: { id: string; name: string }) => (
<div
key={doc.id}
className="flex items-center gap-2 text-sm"
>
<FileIcon className="h-4 w-4 text-muted-foreground" />
<span className="truncate">
{doc.name}
</span>
</div>
),
)}
</div>
</ScrollArea>
</div>
</PopoverContent>
</Popover>
) : (
<>
{documentCount} document
{documentCount !== 1 ? "s" : ""}
</>
)}{" "}
{bulkGroup.type === "FOLDER" &&
bulkGroup.metadata?.folderName ? (
<>
from folder{" "}
<span className="font-medium">
{bulkGroup.metadata.folderName}
</span>
</>
) : bulkGroup.type === "BULK" &&
bulkGroup.metadata?.dataroomName ? (
<>
from{" "}
<span className="font-medium">
{bulkGroup.metadata.dataroomName}
</span>{" "}
dataroom
</>
) : (
"via bulk download"
)}
</span>
</div>
</TableCell>
<TableCell>
<TimestampTooltip
timestamp={event.timestamp}
side="right"
rows={["local", "utc", "unix"]}
>
<time
className="select-none truncate text-sm text-muted-foreground"
dateTime={event.timestamp.toISOString()}
>
{timeAgo(event.timestamp)}
</time>
</TimestampTooltip>
</TableCell>
<TableCell className="table-cell"></TableCell>
</TableRow>
);
}
if (event.type === "view") {
const view = event.data;
return (
<TableRow key={`view-${view.id}`}>
<TableCell>
<div className="flex items-center gap-x-4 overflow-visible">
<FileCheckIcon className="h-5 w-5 text-[#fb7a00]" />
Viewed {view.document.name}
</div>
</TableCell>
<TableCell>
<TimestampTooltip
timestamp={event.timestamp}
side="right"
rows={["local", "utc", "unix"]}
>
<time
className="select-none truncate text-sm text-muted-foreground"
dateTime={event.timestamp.toISOString()}
>
{timeAgo(event.timestamp)}
</time>
</TimestampTooltip>
</TableCell>
<TableCell className="table-cell">
<div className="flex items-center justify-end space-x-4">
<Button size={"sm"} variant={"link"}>
<Link href={`/documents/${view.document.id}`}>
See document
</Link>
</Button>
</div>
</TableCell>
</TableRow>
);
}
if (event.type === "download") {
const view = event.data;
return (
<TableRow key={`download-${view.id}`}>
<TableCell>
<div className="flex items-center gap-x-4 overflow-visible">
<DownloadCloudIcon className="h-5 w-5 text-[#fb7a00]" />
Downloaded {view.document.name}
</div>
</TableCell>
<TableCell>
<TimestampTooltip
timestamp={event.timestamp}
side="right"
rows={["local", "utc", "unix"]}
>
<time
className="select-none truncate text-sm text-muted-foreground"
dateTime={event.timestamp.toISOString()}
>
{timeAgo(event.timestamp)}
</time>
</TimestampTooltip>
</TableCell>
<TableCell className="table-cell">
<div className="flex items-center justify-end space-x-4">
<Button size={"sm"} variant={"link"}>
<Link href={`/documents/${view.document.id}`}>
See document
</Link>
</Button>
</div>
</TableCell>
</TableRow>
);
}
return null;
})
) : documentViews === undefined ? (
<TableRow>
<TableCell>
<Skeleton className="h-6 w-24" />
</TableCell>
<TableCell>
<Skeleton className="h-6 w-16" />
</TableCell>
</TableRow>
) : null}
</>
);
}