Files
dataroom/components/view/link-preview.tsx
zeekay 60163bce2e rebrand: Papermark → Hanzo Dataroom across user-facing surfaces
108 files: README, marketing copy, page titles, meta tags, footer
text, email templates, .env.example, UI strings. URLs swapped
from papermark.com/papermark.io to dataroom.hanzo.ai.

Internal identifiers left intact per repo policy: localStorage
keys (papermark.email/papermark.name/last_papermark_login —
changing them logs users out), type field names
(papermarkUserId), internal helper symbols (PAPERMARK_HEADERS,
PapermarkSparkle component, isPapermarkUrl helper),
internal-only filenames. LLM.md retains 'Upstream: Papermark'
attribution line. Per-locale .po translations regenerate from
source on the next lingui-extract CI run.

DB migrations untouched (append-only history; rewriting them
breaks installed schemas). LICENSE stays AGPL-3.0.
2026-06-07 12:23:40 -07:00

289 lines
7.3 KiB
TypeScript

import { useRouter } from "next/router";
import { useEffect, useMemo, useRef } from "react";
import { ExternalLink } from "lucide-react";
import { useSafePageViewTracker } from "@/lib/tracking/safe-page-view-tracker";
import { getTrackingOptions } from "@/lib/tracking/tracking-config";
import { Button } from "@/components/ui/button";
import Nav, { TNavData } from "./nav";
import { AwayPoster } from "./viewer/away-poster";
interface LinkPreviewProps {
linkUrl: string;
linkName: string;
navData: TNavData;
versionNumber: number;
}
export default function LinkPreview({
linkUrl,
linkName,
navData,
versionNumber,
}: LinkPreviewProps) {
const router = useRouter();
const startTimeRef = useRef(Date.now());
const visibilityRef = useRef<boolean>(true);
const domain = useMemo(() => {
if (!linkUrl) return "";
try {
const url = new URL(linkUrl);
return url.hostname.replace("www.", "");
} catch (e) {
// If URL parsing fails, try to extract domain from string
const match = linkUrl.match(/https?:\/\/([^\/]+)/);
if (match) {
return match[1].replace("www.", "");
}
return linkUrl.length > 50 ? linkUrl.substring(0, 50) + "..." : linkUrl;
}
}, [linkUrl]);
const trackingOptions = getTrackingOptions();
const {
trackPageViewSafely,
resetTrackingState,
startIntervalTracking,
stopIntervalTracking,
getActiveDuration,
isInactive,
updateActivity,
} = useSafePageViewTracker({
...trackingOptions,
externalStartTimeRef: startTimeRef,
});
const { linkId, documentId, viewId, isPreview, dataroomId } = navData;
useEffect(() => {
// Remove token and email query parameters on component mount
const removeQueryParams = (queries: string[]) => {
const currentQuery = { ...router.query };
const currentPath = router.asPath.split("?")[0];
queries.map((query) => delete currentQuery[query]);
router.replace(
{
pathname: currentPath,
query: currentQuery,
},
undefined,
{ shallow: true },
);
};
if (router.query.token) {
removeQueryParams(["token", "email", "domain", "slug", "linkId"]);
}
}, []);
useEffect(() => {
const handleVisibilityChange = () => {
if (document.visibilityState === "visible") {
visibilityRef.current = true;
resetTrackingState();
// Restart interval tracking
const trackingData = {
linkId,
documentId,
viewId,
pageNumber: 1,
versionNumber,
dataroomId,
isPreview,
};
startIntervalTracking(trackingData);
} else {
visibilityRef.current = false;
stopIntervalTracking();
// Track final duration using activity-aware calculation
const duration = getActiveDuration();
trackPageViewSafely(
{
linkId,
documentId,
viewId,
duration,
pageNumber: 1,
versionNumber,
dataroomId,
isPreview,
},
true,
);
}
};
document.addEventListener("visibilitychange", handleVisibilityChange);
return () => {
document.removeEventListener("visibilitychange", handleVisibilityChange);
};
}, [
linkId,
documentId,
viewId,
versionNumber,
dataroomId,
isPreview,
trackPageViewSafely,
resetTrackingState,
startIntervalTracking,
stopIntervalTracking,
getActiveDuration,
]);
useEffect(() => {
const handleBeforeUnload = () => {
stopIntervalTracking();
const duration = getActiveDuration();
trackPageViewSafely(
{
linkId,
documentId,
viewId,
duration,
pageNumber: 1,
versionNumber,
dataroomId,
isPreview,
},
true,
);
};
window.addEventListener("beforeunload", handleBeforeUnload);
return () => {
window.removeEventListener("beforeunload", handleBeforeUnload);
};
}, [
linkId,
documentId,
viewId,
versionNumber,
dataroomId,
isPreview,
trackPageViewSafely,
stopIntervalTracking,
getActiveDuration,
]);
useEffect(() => {
const trackingData = {
linkId,
documentId,
viewId,
pageNumber: 1,
versionNumber,
dataroomId,
isPreview,
};
startIntervalTracking(trackingData);
return () => {
stopIntervalTracking();
};
}, [
linkId,
documentId,
viewId,
versionNumber,
dataroomId,
isPreview,
startIntervalTracking,
stopIntervalTracking,
]);
const handleLinkClick = () => {
// Track the link click (non-blocking)
if (!isPreview && viewId && linkUrl) {
fetch("/api/record_click", {
method: "POST",
body: JSON.stringify({
timestamp: new Date().toISOString(),
sessionId: viewId,
linkId,
documentId,
viewId,
pageNumber: "1",
href: linkUrl,
versionNumber,
dataroomId,
}),
headers: {
"Content-Type": "application/json",
},
}).catch(console.error);
}
window.open(linkUrl, "_blank", "noopener,noreferrer");
return;
};
return (
<>
<Nav pageNumber={1} numPages={1} navData={navData} />
<div
style={{ height: "calc(100dvh - 64px)" }}
className="relative flex flex-col items-center justify-center bg-gray-50 dark:bg-gray-900"
>
<div className="flex flex-col items-center space-y-6 p-8 text-center">
<div className="rounded-full bg-gray-100 p-6 dark:bg-gray-800">
<ExternalLink className="h-12 w-12 text-gray-600 dark:text-gray-300" />
</div>
<h2 className="text-2xl font-semibold text-gray-900 dark:text-gray-100">
{domain || linkName || "External Link"}
</h2>
<p className="max-w-md text-sm text-gray-500 dark:text-gray-400">
You&apos;re leaving Hanzo Dataroom. If you trust this link, click to
continue.
</p>
{linkUrl ? (
<a
href={linkUrl}
target="_blank"
rel="noopener noreferrer"
className="block cursor-pointer break-all text-sm font-medium text-blue-600 underline transition-colors hover:text-blue-700 dark:text-blue-400 dark:hover:text-blue-300"
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
handleLinkClick();
}}
>
{linkUrl}
</a>
) : (
<p className="text-sm text-gray-500 dark:text-gray-400">
Link URL not available
</p>
)}
<Button
onClick={handleLinkClick}
className="w-full max-w-md space-x-2"
size="lg"
disabled={!linkUrl}
>
<ExternalLink className="h-4 w-4" />
<span>Continue to {domain || "link"}</span>
</Button>
</div>
</div>
<AwayPoster
isVisible={isInactive}
inactivityThreshold={trackingOptions.inactivityThreshold}
onDismiss={updateActivity}
/>
</>
);
}