diff --git a/components/Sidebar.tsx b/components/Sidebar.tsx
index 59fe20a23..963179eee 100644
--- a/components/Sidebar.tsx
+++ b/components/Sidebar.tsx
@@ -1,3 +1,4 @@
+import Link from "next/link";
import { useRouter } from "next/router";
import { useEffect, useState } from "react";
@@ -6,6 +7,7 @@ import { TeamContextType, initialState, useTeam } from "@/context/team-context";
import Cookies from "js-cookie";
import {
CogIcon,
+ ContactIcon,
FolderIcon as FolderLucideIcon,
FolderOpenIcon,
PaletteIcon,
@@ -29,7 +31,6 @@ import { AddTeamModal } from "./teams/add-team-modal";
import SelectTeam from "./teams/select-team";
import { Progress } from "./ui/progress";
import { ScrollArea } from "./ui/scroll-area";
-import Link from "next/link";
export default function Sidebar() {
return (
@@ -123,6 +124,14 @@ export const SidebarComponent = ({ className }: { className?: string }) => {
? false
: true,
},
+ {
+ name: "Visitors",
+ href: "/visitors",
+ icon: ContactIcon,
+ current: router.pathname.includes("visitors"),
+ active: false,
+ disabled: userPlan === "free" ? true : false,
+ },
{
name: "Branding",
href: "/settings/branding",
@@ -157,7 +166,7 @@ export const SidebarComponent = ({ className }: { className?: string }) => {
- Papermark{" "}
+ Papermark
{userPlan && userPlan != "free" ? (
{userPlan.charAt(0).toUpperCase() + userPlan.slice(1)}
diff --git a/components/visitors/contacts-document-table.tsx b/components/visitors/contacts-document-table.tsx
new file mode 100644
index 000000000..4773d32f7
--- /dev/null
+++ b/components/visitors/contacts-document-table.tsx
@@ -0,0 +1,363 @@
+"use client";
+
+import { useRouter } from "next/router";
+
+import { useMemo, useState } from "react";
+import React from "react";
+
+import {
+ ColumnDef,
+ ExpandedState,
+ SortingState,
+ flexRender,
+ getCoreRowModel,
+ getExpandedRowModel,
+ getPaginationRowModel,
+ getSortedRowModel,
+ useReactTable,
+} from "@tanstack/react-table";
+import {
+ ChevronDownIcon,
+ ChevronUpIcon,
+ ChevronsUpDownIcon,
+} from "lucide-react";
+
+import { Button } from "@/components/ui/button";
+import { Skeleton } from "@/components/ui/skeleton";
+import {
+ Table,
+ TableBody,
+ TableCell,
+ TableHead,
+ TableHeader,
+ TableRow,
+} from "@/components/ui/table";
+
+import { durationFormat, timeAgo } from "@/lib/utils";
+import { fileIcon } from "@/lib/utils/get-file-icon";
+
+import { DataTablePagination } from "./data-table-pagination";
+
+type DocumentView = {
+ documentId: string;
+ document: {
+ name: string;
+ type: string;
+ };
+ lastViewed: Date;
+ totalDuration: number;
+ viewCount: number;
+};
+
+const columns: ColumnDef[] = [
+ {
+ accessorKey: "document",
+ header: "Document Name",
+ cell: ({ row }) => {
+ const view = row.original;
+ return (
+
+ {fileIcon({
+ fileType: view.document.type ?? "",
+ className: "h-7 w-7",
+ isLight: true,
+ })}
+
+
+
+ {view.document.name}
+
+
+
+
+ );
+ },
+ },
+ {
+ accessorKey: "lastViewed",
+ header: ({ column }) => {
+ return (
+
+ );
+ },
+ cell: ({ row }) => {
+ const view = row.original;
+ return (
+
+ );
+ },
+ sortingFn: (rowA, rowB) => {
+ return (
+ new Date(rowB.original.lastViewed).getTime() -
+ new Date(rowA.original.lastViewed).getTime()
+ );
+ },
+ },
+ {
+ accessorKey: "totalDuration",
+ header: ({ column }) => {
+ return (
+
+ );
+ },
+ cell: ({ row }) => {
+ const view = row.original;
+ return (
+
+ {durationFormat(view.totalDuration)}
+
+ );
+ },
+ },
+ {
+ accessorKey: "viewCount",
+ header: ({ column }) => {
+ return (
+
+ );
+ },
+ cell: ({ row }) => {
+ const view = row.original;
+ return (
+ {view.viewCount}
+ );
+ },
+ },
+ // INFO: disable for now until we have more details
+ // {
+ // id: "expander",
+ // header: () => null,
+ // cell: ({ row }) => {
+ // return (
+ //
+ // );
+ // },
+ // },
+];
+
+export function ContactsDocumentsTable({
+ views,
+}: {
+ views: DocumentView[] | null | undefined;
+}) {
+ const router = useRouter();
+ const [sorting, setSorting] = useState([
+ { id: "lastViewed", desc: false },
+ ]);
+ const [expanded, setExpanded] = useState({});
+
+ const data = useMemo(() => views || [], [views]);
+
+ const table = useReactTable({
+ data,
+ columns,
+ getCoreRowModel: getCoreRowModel(),
+ getSortedRowModel: getSortedRowModel(),
+ getPaginationRowModel: getPaginationRowModel(),
+ getExpandedRowModel: getExpandedRowModel(),
+ onSortingChange: (updater) => {
+ setSorting((old) => {
+ const newSorting =
+ typeof updater === "function" ? updater(old) : updater;
+ if (newSorting.length > 0) {
+ const [{ id, desc }] = newSorting;
+ const prevSorting = old.find((s) => s.id === id);
+ if (prevSorting) {
+ if (prevSorting.desc && !desc) {
+ // If it was descending and now ascending, reset
+ return [];
+ }
+ }
+ }
+ return newSorting;
+ });
+ },
+ onExpandedChange: setExpanded,
+ state: {
+ sorting,
+ expanded,
+ },
+ });
+
+ if (!views) {
+ return (
+
+
+
+
+ Document Name
+ Last Viewed
+ Time Spent
+ Visits
+
+
+
+
+ {[...Array(5)].map((_, index) => (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ))}
+
+
+
+ );
+ }
+
+ const handleRowClick = (id: string) => {
+ router.push(`/documents/${id}`);
+ };
+
+ return (
+
+
+
+
+ {table.getHeaderGroups().map((headerGroup) => (
+
+ {headerGroup.headers.map((header) => (
+
+ {header.isPlaceholder
+ ? null
+ : flexRender(
+ header.column.columnDef.header,
+ header.getContext(),
+ )}
+
+ ))}
+
+ ))}
+
+
+ {table.getRowModel().rows?.length ? (
+ table.getRowModel().rows.map((row) => (
+
+ handleRowClick(row.original.documentId)}
+ className="cursor-pointer"
+ >
+ {row.getVisibleCells().map((cell) => (
+
+ {flexRender(
+ cell.column.columnDef.cell,
+ cell.getContext(),
+ )}
+
+ ))}
+
+ {row.getIsExpanded() && (
+
+
+ {/* Placeholder for expanded content */}
+
+
+ Additional Details
+
+
Chart placeholder
+
Rows of all visits placeholder
+
+
+
+ )}
+
+ ))
+ ) : (
+
+
+ No results.
+
+
+ )}
+
+
+
+
+
+ );
+}
diff --git a/components/visitors/contacts-table.tsx b/components/visitors/contacts-table.tsx
new file mode 100644
index 000000000..cdd6da669
--- /dev/null
+++ b/components/visitors/contacts-table.tsx
@@ -0,0 +1,284 @@
+"use client";
+
+import { useRouter } from "next/router";
+
+import { useMemo, useState } from "react";
+import React from "react";
+
+import {
+ ColumnDef,
+ SortingState,
+ flexRender,
+ getCoreRowModel,
+ getPaginationRowModel,
+ getSortedRowModel,
+ useReactTable,
+} from "@tanstack/react-table";
+import {
+ ArrowUpDown,
+ ChevronDownIcon,
+ ChevronUpIcon,
+ ChevronsUpDownIcon,
+} from "lucide-react";
+
+import { Button } from "@/components/ui/button";
+import {
+ Table,
+ TableBody,
+ TableCell,
+ TableHead,
+ TableHeader,
+ TableRow,
+} from "@/components/ui/table";
+import { VisitorAvatar } from "@/components/visitors/visitor-avatar";
+
+import { timeAgo } from "@/lib/utils";
+
+import { Skeleton } from "../ui/skeleton";
+import { DataTablePagination } from "./data-table-pagination";
+
+type Viewer = {
+ id: string;
+ email: string;
+ views: { viewedAt: Date }[];
+};
+
+const columns: ColumnDef[] = [
+ {
+ accessorKey: "email",
+ header: "Contact",
+ cell: ({ row }) => (
+
+
+
+
+
+ {row.original.email}
+
+
+
+
+ ),
+ },
+ {
+ accessorKey: "lastViewed",
+ header: ({ column }) => {
+ return (
+
+ );
+ },
+ cell: ({ row }) => {
+ const lastView = row.original.views[0];
+ return lastView ? (
+
+ ) : (
+ -
+ );
+ },
+ sortingFn: (rowA, rowB) => {
+ const dateA = rowA.original.views[0]?.viewedAt;
+ const dateB = rowB.original.views[0]?.viewedAt;
+
+ if (!dateA && !dateB) return 0;
+ if (!dateA) return 1;
+ if (!dateB) return -1;
+
+ const timeA =
+ dateA instanceof Date ? dateA.getTime() : new Date(dateA).getTime();
+ const timeB =
+ dateB instanceof Date ? dateB.getTime() : new Date(dateB).getTime();
+
+ return timeB - timeA; // Sort in descending order (most recent first)
+ },
+ },
+ {
+ accessorKey: "totalVisits",
+ header: ({ column }) => {
+ return (
+
+ );
+ },
+ cell: ({ row }) => (
+
+ {row.original.views.length}
+
+ ),
+ sortingFn: (rowA, rowB) =>
+ rowA.original.views.length - rowB.original.views.length,
+ },
+];
+
+export function ContactsTable({
+ viewers,
+}: {
+ viewers: Viewer[] | null | undefined;
+}) {
+ const router = useRouter();
+ const [sorting, setSorting] = useState([
+ { id: "lastViewed", desc: false },
+ ]);
+
+ const data = useMemo(() => viewers || [], [viewers]);
+
+ const table = useReactTable({
+ data,
+ columns,
+ getCoreRowModel: getCoreRowModel(),
+ getSortedRowModel: getSortedRowModel(),
+ getPaginationRowModel: getPaginationRowModel(),
+ onSortingChange: (updater) => {
+ setSorting((old) => {
+ const newSorting =
+ typeof updater === "function" ? updater(old) : updater;
+ if (newSorting.length > 0) {
+ const [{ id, desc }] = newSorting;
+ const prevSorting = old.find((s) => s.id === id);
+ if (prevSorting) {
+ if (prevSorting.desc && !desc) {
+ // If it was descending and now ascending, reset
+ return [];
+ }
+ }
+ }
+ return newSorting;
+ });
+ },
+ state: {
+ sorting,
+ },
+ });
+
+ if (!viewers) {
+ return (
+
+
+
+
+ Name
+ Last Viewed
+ Total Visits
+
+
+
+ {[...Array(5)].map((_, index) => (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ))}
+
+
+
+ );
+ }
+
+ const handleRowClick = (id: string) => {
+ router.push(`/visitors/${id}`);
+ };
+
+ return (
+
+
+
+
+ {table.getHeaderGroups().map((headerGroup) => (
+
+ {headerGroup.headers.map((header) => (
+
+ {header.isPlaceholder
+ ? null
+ : flexRender(
+ header.column.columnDef.header,
+ header.getContext(),
+ )}
+
+ ))}
+
+ ))}
+
+
+ {table.getRowModel().rows?.length ? (
+ table.getRowModel().rows.map((row) => (
+ handleRowClick(row.original.id)}
+ className="cursor-pointer"
+ >
+ {row.getVisibleCells().map((cell) => {
+ return (
+
+ {flexRender(
+ cell.column.columnDef.cell,
+ cell.getContext(),
+ )}
+
+ );
+ })}
+
+ ))
+ ) : (
+
+
+ No results.
+
+
+ )}
+
+
+
+
+
+ );
+}
diff --git a/components/visitors/data-table-pagination.tsx b/components/visitors/data-table-pagination.tsx
new file mode 100644
index 000000000..0c8ab99a4
--- /dev/null
+++ b/components/visitors/data-table-pagination.tsx
@@ -0,0 +1,103 @@
+import { Table } from "@tanstack/react-table";
+import {
+ ChevronLeftIcon,
+ ChevronRightIcon,
+ ChevronsLeftIcon,
+ ChevronsRightIcon,
+} from "lucide-react";
+
+import { Button } from "@/components/ui/button";
+import {
+ Select,
+ SelectContent,
+ SelectItem,
+ SelectTrigger,
+ SelectValue,
+} from "@/components/ui/select";
+
+interface DataTablePaginationProps {
+ table: Table;
+ name: string;
+}
+
+export function DataTablePagination({
+ table,
+ name,
+}: DataTablePaginationProps) {
+ const { pageSize, pageIndex } = table.getState().pagination;
+ const totalRows = table.getFilteredRowModel().rows.length;
+ const visibleRows = table.getRowModel().rows.length;
+
+ return (
+
+
+ {visibleRows} of {totalRows} {name}s shown
+
+
+
+
+ {name.charAt(0).toUpperCase() + name.slice(1)}s per page
+
+
+
+
+ Page {pageIndex + 1} of {table.getPageCount()}
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/lib/swr/use-viewer.ts b/lib/swr/use-viewer.ts
new file mode 100644
index 000000000..008e2744a
--- /dev/null
+++ b/lib/swr/use-viewer.ts
@@ -0,0 +1,35 @@
+import { useRouter } from "next/router";
+
+import { useTeam } from "@/context/team-context";
+import { View, Viewer } from "@prisma/client";
+import useSWR from "swr";
+
+import { fetcher } from "@/lib/utils";
+
+type ViewerWithViews = Viewer & {
+ views: {
+ documentId: string;
+ viewCount: number;
+ viewIds: string[];
+ lastViewed: Date;
+ }[];
+};
+
+export default function useViewer() {
+ const router = useRouter();
+ const teamInfo = useTeam();
+ const teamId = teamInfo?.currentTeam?.id;
+
+ const { id } = router.query;
+
+ const { data: viewer, error } = useSWR(
+ teamId && id && `/api/teams/${teamId}/viewers/${id}`,
+ fetcher,
+ );
+
+ return {
+ viewer,
+ loading: !viewer && !error,
+ error,
+ };
+}
diff --git a/lib/swr/use-viewers.ts b/lib/swr/use-viewers.ts
new file mode 100644
index 000000000..4537c6f14
--- /dev/null
+++ b/lib/swr/use-viewers.ts
@@ -0,0 +1,21 @@
+import { useTeam } from "@/context/team-context";
+import { Viewer } from "@prisma/client";
+import useSWR from "swr";
+
+import { fetcher } from "@/lib/utils";
+
+export default function useViewers() {
+ const teamInfo = useTeam();
+ const teamId = teamInfo?.currentTeam?.id;
+
+ const { data: viewers, error } = useSWR(
+ teamId && `/api/teams/${teamId}/viewers`,
+ fetcher,
+ );
+
+ return {
+ viewers,
+ loading: !viewers && !error,
+ error,
+ };
+}
diff --git a/lib/tinybird/endpoints/get_document_duration_per_viewer.pipe b/lib/tinybird/endpoints/get_document_duration_per_viewer.pipe
new file mode 100644
index 000000000..4a2695435
--- /dev/null
+++ b/lib/tinybird/endpoints/get_document_duration_per_viewer.pipe
@@ -0,0 +1,12 @@
+VERSION 1
+
+NODE endpoint
+SQL >
+ %
+ SELECT
+ SUM(duration) AS sum_duration
+ FROM
+ page_views__v3
+ WHERE
+ documentId = {{ String(documentId, required=True)}}
+ AND viewId IN splitByChar(',', {{ String(viewIds, required=True) }})
diff --git a/lib/tinybird/pipes.ts b/lib/tinybird/pipes.ts
index 41fa5eae6..f2c46053f 100644
--- a/lib/tinybird/pipes.ts
+++ b/lib/tinybird/pipes.ts
@@ -60,3 +60,14 @@ export const getTotalDataroomDuration = tb.buildPipe({
sum_duration: z.number(),
}),
});
+
+export const getDocumentDurationPerViewer = tb.buildPipe({
+ pipe: "get_document_duration_per_viewer__v1",
+ parameters: z.object({
+ documentId: z.string(),
+ viewIds: z.string().describe("Comma separated viewIds"),
+ }),
+ data: z.object({
+ sum_duration: z.number(),
+ }),
+});
diff --git a/pages/api/teams/[teamId]/viewers/[id]/index.ts b/pages/api/teams/[teamId]/viewers/[id]/index.ts
new file mode 100644
index 000000000..7c42a4d53
--- /dev/null
+++ b/pages/api/teams/[teamId]/viewers/[id]/index.ts
@@ -0,0 +1,161 @@
+import { NextApiRequest, NextApiResponse } from "next";
+
+import { authOptions } from "@/pages/api/auth/[...nextauth]";
+import { getServerSession } from "next-auth/next";
+
+import { errorhandler } from "@/lib/errorHandler";
+import prisma from "@/lib/prisma";
+import { getDocumentDurationPerViewer } from "@/lib/tinybird";
+import { CustomUser } from "@/lib/types";
+
+export default async function handle(
+ req: NextApiRequest,
+ res: NextApiResponse,
+) {
+ if (req.method === "GET") {
+ // GET /api/teams/:teamId/viewers/:id
+ const session = await getServerSession(req, res, authOptions);
+ if (!session) {
+ return res.status(401).end("Unauthorized");
+ }
+
+ const { teamId, id } = req.query as { teamId: string; id: string };
+
+ const userId = (session.user as CustomUser).id;
+
+ try {
+ const team = await prisma.team.findUnique({
+ where: {
+ id: teamId,
+ users: {
+ some: {
+ userId,
+ },
+ },
+ },
+ });
+
+ if (!team) {
+ return res.status(404).json({ error: "Team not found" });
+ }
+
+ const viewer = await prisma.viewer.findUnique({
+ where: { id },
+ include: {
+ views: {
+ where: {
+ documentId: {
+ not: null,
+ },
+ },
+ },
+ },
+ });
+
+ // same but for documentId
+ const groupedViewsByDocumentId = viewer?.views.reduce(
+ (
+ acc: Record<
+ string,
+ {
+ documentId: string;
+ viewCount: number;
+ viewIds: string[];
+ lastViewed: Date;
+ }
+ >,
+ view,
+ ) => {
+ const key = view.documentId!;
+ if (!acc[key]) {
+ acc[key] = {
+ documentId: view.documentId ?? "",
+ viewCount: 0,
+ viewIds: [],
+ lastViewed: new Date(0),
+ };
+ }
+ acc[key].viewCount++;
+ acc[key].viewIds.push(view.id);
+ if (view.viewedAt > acc[key].lastViewed) {
+ acc[key].lastViewed = view.viewedAt;
+ }
+ return acc;
+ },
+ {},
+ );
+
+ // get the document details for each documentId
+ let documentDetails: {
+ id: string;
+ name: string | null;
+ type: string | null;
+ contentType: string | null;
+ }[] = [];
+ if (groupedViewsByDocumentId) {
+ documentDetails = await prisma.document.findMany({
+ where: {
+ id: {
+ in: Object.keys(groupedViewsByDocumentId),
+ },
+ },
+ select: {
+ id: true,
+ name: true,
+ type: true,
+ contentType: true,
+ },
+ });
+ }
+
+ // Calculate sum_duration for each document
+ const documentDurations = await Promise.all(
+ Object.entries(groupedViewsByDocumentId ?? {}).map(
+ async ([documentId, view]) => {
+ const durationResult = await getDocumentDurationPerViewer({
+ documentId,
+ viewIds: view.viewIds.join(","),
+ });
+ return {
+ documentId,
+ sum_duration: durationResult.data[0].sum_duration,
+ };
+ },
+ ),
+ );
+
+ // Create a map for quick lookup
+ const durationMap = new Map(
+ documentDurations.map((d) => [d.documentId, d.sum_duration]),
+ );
+
+ // create a new array with the grouped views and merge with document details
+ const groupedViews = groupedViewsByDocumentId
+ ? Object.values(groupedViewsByDocumentId).map((view) => {
+ const document = documentDetails?.find(
+ (doc) => doc.id === view.documentId,
+ );
+
+ return {
+ ...view,
+ document,
+ totalDuration: durationMap.get(view.documentId) || 0,
+ };
+ })
+ : [];
+
+ const newViewer = {
+ ...viewer,
+ views: groupedViews,
+ };
+
+ return res.status(200).json(newViewer);
+ } catch (error) {
+ errorhandler(error, res);
+ }
+ } else {
+ // We only allow GET requests
+ res.setHeader("Allow", ["GET"]);
+ return res.status(405).end(`Method ${req.method} Not Allowed`);
+ }
+}
diff --git a/pages/api/teams/[teamId]/viewers/index.ts b/pages/api/teams/[teamId]/viewers/index.ts
new file mode 100644
index 000000000..fe1ff26af
--- /dev/null
+++ b/pages/api/teams/[teamId]/viewers/index.ts
@@ -0,0 +1,66 @@
+import { NextApiRequest, NextApiResponse } from "next";
+
+import { authOptions } from "@/pages/api/auth/[...nextauth]";
+import { getServerSession } from "next-auth/next";
+
+import { errorhandler } from "@/lib/errorHandler";
+import prisma from "@/lib/prisma";
+import { CustomUser } from "@/lib/types";
+
+export default async function handle(
+ req: NextApiRequest,
+ res: NextApiResponse,
+) {
+ if (req.method === "GET") {
+ // GET /api/teams/:teamId/viewers
+ const session = await getServerSession(req, res, authOptions);
+ if (!session) {
+ return res.status(401).end("Unauthorized");
+ }
+
+ const { teamId } = req.query as { teamId: string };
+
+ const userId = (session.user as CustomUser).id;
+
+ try {
+ const team = await prisma.team.findUnique({
+ where: {
+ id: teamId,
+ users: {
+ some: {
+ userId,
+ },
+ },
+ },
+ });
+
+ if (!team) {
+ return res.status(404).json({ error: "Team not found" });
+ }
+
+ const viewers = await prisma.viewer.findMany({
+ where: { teamId },
+ include: {
+ views: {
+ orderBy: {
+ viewedAt: "desc",
+ },
+ where: {
+ documentId: {
+ not: null,
+ },
+ },
+ },
+ },
+ });
+
+ return res.status(200).json(viewers);
+ } catch (error) {
+ errorhandler(error, res);
+ }
+ } else {
+ // We only allow GET requests
+ res.setHeader("Allow", ["GET"]);
+ return res.status(405).end(`Method ${req.method} Not Allowed`);
+ }
+}
diff --git a/pages/api/views-dataroom.ts b/pages/api/views-dataroom.ts
index 372f5abf9..96e2698f4 100644
--- a/pages/api/views-dataroom.ts
+++ b/pages/api/views-dataroom.ts
@@ -8,7 +8,6 @@ import { parsePageId } from "notion-utils";
import { hashToken } from "@/lib/api/auth/token";
import sendNotification from "@/lib/api/notification-helper";
import { sendOtpVerificationEmail } from "@/lib/emails/send-email-otp-verification";
-import { sendVerificationEmail } from "@/lib/emails/send-email-verification";
import { getFile } from "@/lib/files/get-file";
import { newId } from "@/lib/id-helper";
import notion from "@/lib/notion";
diff --git a/pages/datarooms/index.tsx b/pages/datarooms/index.tsx
index 7f918f26e..40fb82dad 100644
--- a/pages/datarooms/index.tsx
+++ b/pages/datarooms/index.tsx
@@ -1,4 +1,7 @@
import Link from "next/link";
+import { useRouter } from "next/router";
+
+import { useEffect } from "react";
import { PlusIcon } from "lucide-react";
@@ -21,27 +24,25 @@ import { usePlan } from "@/lib/swr/use-billing";
import useDatarooms from "@/lib/swr/use-datarooms";
import useLimits from "@/lib/swr/use-limits";
import { daysLeft } from "@/lib/utils";
-import { useEffect } from "react";
-import { useRouter } from "next/navigation";
export default function DataroomsPage() {
const { datarooms } = useDatarooms();
const { plan, trial } = usePlan();
const { limits } = useLimits();
- const router = useRouter()
+ const router = useRouter();
const numDatarooms = datarooms?.length ?? 0;
const limitDatarooms = limits?.datarooms ?? 1;
+ const isTrial = !!trial;
const isBusiness = plan === "business";
const isDatarooms = plan === "datarooms";
- const isTrialDatarooms = trial === "drtrial";
const canCreateUnlimitedDatarooms =
isDatarooms || (isBusiness && numDatarooms < limitDatarooms);
- useEffect(()=>{
- if(trial == null && plan == 'free') router.push('/documents')
- },[trial,plan])
+ useEffect(() => {
+ if (plan == "free" && !isTrial) router.push("/documents");
+ }, [isTrial, plan]);
return (
@@ -65,7 +66,7 @@ export default function DataroomsPage() {
Upgrade to Add Data Room
- ) : isTrialDatarooms && datarooms && !isBusiness && !isDatarooms ? (
+ ) : isTrial && datarooms && !isBusiness && !isDatarooms ? (
Dataroom Trial:
diff --git a/pages/visitors/[id]/index.tsx b/pages/visitors/[id]/index.tsx
new file mode 100644
index 000000000..e855bb5ef
--- /dev/null
+++ b/pages/visitors/[id]/index.tsx
@@ -0,0 +1,56 @@
+import Link from "next/link";
+
+import { useState } from "react";
+
+import AppLayout from "@/components/layouts/app";
+import {
+ Breadcrumb,
+ BreadcrumbItem,
+ BreadcrumbLink,
+ BreadcrumbList,
+ BreadcrumbPage,
+ BreadcrumbSeparator,
+} from "@/components/ui/breadcrumb";
+import { Separator } from "@/components/ui/separator";
+import { ContactsDocumentsTable } from "@/components/visitors/contacts-document-table";
+
+import useViewer from "@/lib/swr/use-viewer";
+
+export default function VisitorDetailPage() {
+ const { viewer } = useViewer();
+ const views = viewer?.views;
+
+ return (
+
+
+
+
+
+
+
+ All Visitors
+
+
+
+
+ {viewer?.email}
+
+
+
+
+
+ {viewer?.email}
+
+
+
+
+
+
+
+
+ {/* @ts-ignore */}
+
+
+
+ );
+}
diff --git a/pages/visitors/index.tsx b/pages/visitors/index.tsx
new file mode 100644
index 000000000..6e77a2a8a
--- /dev/null
+++ b/pages/visitors/index.tsx
@@ -0,0 +1,46 @@
+import { useRouter } from "next/router";
+
+import { useEffect, useState } from "react";
+
+import AppLayout from "@/components/layouts/app";
+import { Separator } from "@/components/ui/separator";
+import { ContactsTable } from "@/components/visitors/contacts-table";
+
+import { usePlan } from "@/lib/swr/use-billing";
+import useViewers from "@/lib/swr/use-viewers";
+
+export default function Visitors() {
+ const router = useRouter();
+ const { plan, trial } = usePlan();
+ const { viewers } = useViewers();
+
+ const isTrial = !!trial;
+ const isFree = plan == "free";
+
+ useEffect(() => {
+ if (isFree && !isTrial) router.push("/documents");
+ }, [isTrial, isFree]);
+
+ return (
+
+
+
+
+
+ All visitors
+
+
+ Manage all your visitors in one place.
+
+
+
+
+
+
+
+ {/* @ts-ignore */}
+
+
+
+ );
+}