mirror of
https://github.com/hanzoai/dataroom.git
synced 2026-10-30 18:41:07 +00:00
327 lines
8.6 KiB
TypeScript
327 lines
8.6 KiB
TypeScript
import { useEffect, useRef, useState } from "react";
|
|
import React from "react";
|
|
|
|
import "@/public/vendor/handsontable/handsontable.full.min.css";
|
|
import { Brand, DataroomBrand } from "@prisma/client";
|
|
|
|
import { useSafePageViewTracker } from "@/lib/tracking/safe-page-view-tracker";
|
|
import { getTrackingOptions } from "@/lib/tracking/tracking-config";
|
|
import { cn } from "@/lib/utils";
|
|
|
|
import { Button } from "@/components/ui/button";
|
|
|
|
import { ScreenProtector } from "../ScreenProtection";
|
|
import { TDocumentData } from "../dataroom/dataroom-view";
|
|
import Nav, { TNavData } from "../nav";
|
|
import { AwayPoster } from "./away-poster";
|
|
|
|
// Define the type for the JSON data
|
|
type RowData = { [key: string]: any };
|
|
type SheetData = {
|
|
sheetName: string;
|
|
columnData: string[];
|
|
rowData: RowData[];
|
|
};
|
|
|
|
export default function ExcelViewer({
|
|
versionNumber,
|
|
sheetData,
|
|
screenshotProtectionEnabled,
|
|
navData,
|
|
}: {
|
|
versionNumber: number;
|
|
sheetData: SheetData[];
|
|
screenshotProtectionEnabled: boolean;
|
|
navData: TNavData;
|
|
}) {
|
|
const [availableWidth, setAvailableWidth] = useState<number>(200);
|
|
const [availableHeight, setAvailableHeight] = useState<number>(200);
|
|
const [handsontableLoaded, setHandsontableLoaded] = useState<boolean>(false);
|
|
const [selectedSheetIndex, setSelectedSheetIndex] = useState<number>(0);
|
|
|
|
const [isWindowFocused, setIsWindowFocused] = useState(true);
|
|
|
|
const { linkId, documentId, viewId, isPreview, dataroomId } = navData;
|
|
|
|
const startTimeRef = useRef(Date.now());
|
|
const visibilityRef = useRef<boolean>(true);
|
|
|
|
const {
|
|
trackPageViewSafely,
|
|
resetTrackingState,
|
|
startIntervalTracking,
|
|
stopIntervalTracking,
|
|
getActiveDuration,
|
|
isInactive,
|
|
updateActivity,
|
|
} = useSafePageViewTracker({
|
|
...getTrackingOptions(),
|
|
externalStartTimeRef: startTimeRef,
|
|
});
|
|
|
|
useEffect(() => {
|
|
const script = document.createElement("script");
|
|
script.src =
|
|
"https://cdnjs.cloudflare.com/ajax/libs/handsontable/6.2.2/handsontable.full.min.js";
|
|
script.async = true;
|
|
script.onload = () => {
|
|
setHandsontableLoaded(true);
|
|
};
|
|
script.onerror = () => {
|
|
console.error("Failed to load Handsontable script.");
|
|
};
|
|
|
|
document.body.appendChild(script);
|
|
|
|
return () => {
|
|
document.body.removeChild(script);
|
|
};
|
|
}, []);
|
|
|
|
const hotRef = useRef<HTMLDivElement>(null);
|
|
const containerRef = useRef<HTMLDivElement>(null);
|
|
// @ts-ignore - Handsontable import has not types
|
|
const hotInstanceRef = useRef<Handsontable | null>(null);
|
|
|
|
const calculateSize = () => {
|
|
if (containerRef.current) {
|
|
const offset = containerRef.current.getBoundingClientRect();
|
|
setAvailableWidth(Math.max(offset.width, 200));
|
|
setAvailableHeight(Math.max(offset.height - 50, 200));
|
|
}
|
|
};
|
|
|
|
useEffect(() => {
|
|
const handleResize = () => {
|
|
calculateSize();
|
|
};
|
|
|
|
window.addEventListener("resize", handleResize);
|
|
calculateSize();
|
|
|
|
return () => window.removeEventListener("resize", handleResize);
|
|
}, []);
|
|
|
|
// Add this effect near your other useEffect hooks
|
|
useEffect(() => {
|
|
if (!screenshotProtectionEnabled) return;
|
|
|
|
const handleFocus = () => setIsWindowFocused(true);
|
|
const handleBlur = () => setIsWindowFocused(false);
|
|
|
|
window.addEventListener("focus", handleFocus);
|
|
window.addEventListener("blur", handleBlur);
|
|
|
|
return () => {
|
|
window.removeEventListener("focus", handleFocus);
|
|
window.removeEventListener("blur", handleBlur);
|
|
};
|
|
}, [screenshotProtectionEnabled]);
|
|
|
|
// Start interval tracking when component mounts
|
|
useEffect(() => {
|
|
const trackingData = {
|
|
linkId,
|
|
documentId,
|
|
viewId,
|
|
pageNumber: selectedSheetIndex + 1,
|
|
versionNumber,
|
|
dataroomId,
|
|
isPreview,
|
|
};
|
|
|
|
startIntervalTracking(trackingData);
|
|
|
|
return () => {
|
|
stopIntervalTracking();
|
|
};
|
|
}, [
|
|
linkId,
|
|
documentId,
|
|
viewId,
|
|
versionNumber,
|
|
dataroomId,
|
|
isPreview,
|
|
startIntervalTracking,
|
|
stopIntervalTracking,
|
|
]);
|
|
|
|
useEffect(() => {
|
|
const handleVisibilityChange = () => {
|
|
if (document.visibilityState === "visible") {
|
|
visibilityRef.current = true;
|
|
resetTrackingState();
|
|
|
|
// Restart interval tracking
|
|
const trackingData = {
|
|
linkId,
|
|
documentId,
|
|
viewId,
|
|
pageNumber: selectedSheetIndex + 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: selectedSheetIndex + 1,
|
|
versionNumber,
|
|
dataroomId,
|
|
isPreview,
|
|
},
|
|
true,
|
|
);
|
|
}
|
|
};
|
|
|
|
document.addEventListener("visibilitychange", handleVisibilityChange);
|
|
|
|
return () => {
|
|
document.removeEventListener("visibilitychange", handleVisibilityChange);
|
|
};
|
|
}, [
|
|
selectedSheetIndex,
|
|
linkId,
|
|
documentId,
|
|
viewId,
|
|
versionNumber,
|
|
dataroomId,
|
|
isPreview,
|
|
trackPageViewSafely,
|
|
resetTrackingState,
|
|
startIntervalTracking,
|
|
stopIntervalTracking,
|
|
getActiveDuration,
|
|
]);
|
|
|
|
useEffect(() => {
|
|
const handleBeforeUnload = () => {
|
|
stopIntervalTracking();
|
|
const duration = getActiveDuration();
|
|
trackPageViewSafely(
|
|
{
|
|
linkId,
|
|
documentId,
|
|
viewId,
|
|
duration,
|
|
pageNumber: selectedSheetIndex + 1,
|
|
versionNumber,
|
|
dataroomId,
|
|
isPreview,
|
|
},
|
|
true,
|
|
);
|
|
};
|
|
|
|
window.addEventListener("beforeunload", handleBeforeUnload);
|
|
|
|
return () => {
|
|
window.removeEventListener("beforeunload", handleBeforeUnload);
|
|
};
|
|
}, [
|
|
selectedSheetIndex,
|
|
linkId,
|
|
documentId,
|
|
viewId,
|
|
versionNumber,
|
|
dataroomId,
|
|
isPreview,
|
|
trackPageViewSafely,
|
|
stopIntervalTracking,
|
|
getActiveDuration,
|
|
]);
|
|
|
|
useEffect(() => {
|
|
if (handsontableLoaded && sheetData.length) {
|
|
if (hotInstanceRef.current) {
|
|
hotInstanceRef.current.destroy();
|
|
}
|
|
|
|
const { columnData, rowData } = sheetData[selectedSheetIndex];
|
|
|
|
// @ts-ignore - Handsontable import has not types
|
|
hotInstanceRef.current = new Handsontable(hotRef.current!, {
|
|
data: rowData,
|
|
readOnly: true,
|
|
disableVisualSelection: true,
|
|
comments: false,
|
|
contextMenu: false,
|
|
colHeaders: columnData,
|
|
rowHeaders: true,
|
|
manualColumnResize: true,
|
|
width: availableWidth,
|
|
height: availableHeight,
|
|
rowHeights: 23,
|
|
stretchH: "none",
|
|
viewportRowRenderingOffset: 10,
|
|
viewportColumnRenderingOffset: 2,
|
|
readOnlyCellClassName: "",
|
|
// modifyColWidth: (width: number) => {
|
|
// if (width > 300) {
|
|
// return 300;
|
|
// }
|
|
// },
|
|
});
|
|
}
|
|
}, [
|
|
handsontableLoaded,
|
|
sheetData,
|
|
selectedSheetIndex,
|
|
availableHeight,
|
|
availableWidth,
|
|
]);
|
|
|
|
return (
|
|
<>
|
|
<Nav type="sheet" navData={navData} />
|
|
<div
|
|
style={{ height: "calc(100dvh - 64px)" }}
|
|
className={cn(
|
|
"mx-2 flex h-dvh flex-col sm:mx-6 lg:mx-8",
|
|
!isWindowFocused &&
|
|
screenshotProtectionEnabled &&
|
|
"blur-xl transition-all duration-300",
|
|
)}
|
|
ref={containerRef}
|
|
>
|
|
<div className="" ref={hotRef}></div>
|
|
<div className="flex max-w-fit divide-x divide-gray-200 overflow-x-scroll whitespace-nowrap rounded-b-sm bg-[#f0f0f0] px-1">
|
|
{sheetData.map((sheet, index) => (
|
|
<div className="px-1" key={sheet.sheetName}>
|
|
<Button
|
|
onClick={() => setSelectedSheetIndex(index)}
|
|
className={cn(
|
|
"mb-1 rounded-none rounded-b-sm bg-[#f0f0f0] font-normal text-gray-950 hover:bg-gray-50",
|
|
index === selectedSheetIndex &&
|
|
"bg-white font-medium text-black ring-1 ring-gray-500 hover:bg-white",
|
|
)}
|
|
>
|
|
{sheet.sheetName}
|
|
</Button>
|
|
</div>
|
|
))}
|
|
</div>
|
|
{screenshotProtectionEnabled ? <ScreenProtector /> : null}
|
|
<AwayPoster
|
|
isVisible={isInactive}
|
|
inactivityThreshold={
|
|
getTrackingOptions().inactivityThreshold || 20000
|
|
}
|
|
onDismiss={updateActivity}
|
|
/>
|
|
</div>
|
|
</>
|
|
);
|
|
}
|