Files
papercrate/frontend/src/preview/DocumentViewerLayout.tsx
T
2025-11-13 01:07:55 +01:00

189 lines
5.0 KiB
TypeScript

import { useCallback, useMemo } from 'react';
import type { JSX } from 'react';
import DocumentInfoPanel from '../documents/DocumentInfoPanel';
import { DownloadIcon } from '../ui/icons';
interface DocumentLike {
id?: string | number;
title?: string;
content_type?: string;
filename?: string;
original_name?: string;
[key: string]: unknown;
}
interface PreviewEntry {
url?: string;
contentType?: string;
filename?: string;
}
interface ContentTabConfig {
id?: string;
label?: string;
enabled?: boolean;
forceDisplay?: boolean;
loadContent?: (options?: { signal?: AbortSignal }) => unknown | Promise<unknown>;
loadingMessage?: string;
emptyMessage?: string;
unavailableMessage?: string;
errorMessage?: string;
[key: string]: unknown;
}
type LayoutMode = 'split' | 'stacked' | (string & {});
interface DocumentViewerLayoutProps {
document?: DocumentLike | null;
previewEntry?: PreviewEntry | null;
summaryProps?: Record<string, unknown>;
metadataPayload?: unknown;
contentTabConfig?: ContentTabConfig | null;
resetKey?: string | number | null;
classNamePrefix?: string;
defaultTabId?: string;
infoPanelProps?: Record<string, unknown>;
previewLoadingMessage?: string;
layoutMode?: LayoutMode;
}
const DocumentViewerLayout = ({
document,
previewEntry,
summaryProps = {},
metadataPayload,
contentTabConfig,
resetKey,
classNamePrefix = 'document-viewer',
defaultTabId = 'details',
infoPanelProps = {},
previewLoadingMessage = 'Preparing preview…',
layoutMode = 'split',
}: DocumentViewerLayoutProps): JSX.Element => {
const isStacked = layoutMode === 'stacked';
const previewContent = useMemo(() => {
if (!document || !previewEntry?.url) {
return null;
}
const normalizedContentType = (previewEntry.contentType
|| document.content_type
|| '')
.toLowerCase();
const isImage = normalizedContentType.startsWith('image/');
const isPdf = normalizedContentType === 'application/pdf'
|| normalizedContentType === 'application/x-pdf';
if (isImage) {
return (
<img
src={previewEntry.url}
alt={`Preview of ${document.title}`}
className="document-viewer__object document-viewer__object--image"
draggable={false}
/>
);
}
if (isPdf) {
return (
<iframe
src={previewEntry.url}
title={`Preview of ${document.title}`}
className="document-viewer__object"
/>
);
}
const displayContentType = document.content_type || previewEntry.contentType || 'this file type';
const displayFilename = previewEntry.filename
|| document.filename
|| document.original_name
|| 'download';
return (
<div className="document-viewer__unsupported">
<div className="document-viewer__unsupported-message">
Preview is not available for {displayContentType} files.
</div>
<div className="document-viewer__unsupported-filename">{displayFilename}</div>
<a
className="button-link document-viewer__unsupported-download"
href={previewEntry.url}
download={displayFilename}
target="_blank"
rel="noopener noreferrer"
>
<DownloadIcon />
<span>Download</span>
</a>
</div>
);
}, [previewEntry, document]);
const renderViewportPane = useCallback(() => (
<div className="document-viewer__viewport">
{!previewEntry?.url ? (
<div className="document-viewer__message">{previewLoadingMessage}</div>
) : (
previewContent
)}
</div>
), [previewEntry?.url, previewLoadingMessage, previewContent]);
const viewportPane = renderViewportPane();
const stackedLeadingTabs = useMemo(() => (
isStacked
? [
{
id: 'preview',
label: 'Preview',
render: () => renderViewportPane(),
},
]
: []
), [isStacked, renderViewportPane]);
const resolvedDefaultTabId = isStacked ? 'preview' : defaultTabId;
const summaryPlacement = 'tabs';
const tabsPlacement = 'bottom';
const summaryLayout = 'compact';
const detailsPane = (
<div className="document-viewer__details-pane">
<div className="document-viewer__details">
<DocumentInfoPanel
document={document}
summaryProps={summaryProps}
metadataPayload={metadataPayload}
contentConfig={contentTabConfig}
defaultTabId={resolvedDefaultTabId}
classNamePrefix={classNamePrefix}
hideTabNavWhenSingle={false}
resetKey={resetKey || document?.id}
summaryPlacement={summaryPlacement}
summaryLayout={summaryLayout}
leadingTabs={stackedLeadingTabs}
tabsPlacement={tabsPlacement}
{...infoPanelProps}
/>
</div>
</div>
);
if (isStacked) {
return detailsPane;
}
return (
<>
{detailsPane}
{viewportPane}
</>
);
};
export default DocumentViewerLayout;