mirror of
https://github.com/hanzo-apps/dataroom.git
synced 2026-10-30 14:47:36 +00:00
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.
289 lines
7.3 KiB
TypeScript
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'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}
|
|
/>
|
|
</>
|
|
);
|
|
}
|